Compare commits

...
Author SHA1 Message Date
dependabot[bot] fc5d35f780 Bump github/codeql-action/analyze from 4.36.2 to 4.37.9
Bumps [github/codeql-action/analyze](https://github.com/github/codeql-action) from 4.36.2 to 4.37.9.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/8aad20d150bbac5944a9f9d289da16a4b0d87c1e...cdf488f595d80d6e07e03d4674febd5ab45fa938)

---
updated-dependencies:
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.37.9
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-08 21:54:46 +00:00
Alexander Gubarev 36351ae051 Fix float round/2, floor/2, ceil/2 raises wrong exception for invalid precision (#15872) 2026-09-08 18:06:30 +02:00
Barna Kovacs 5034b7fa6b Defer function domain disjointness until nonemptiness succeeds (#15870)
Skip pairwise disjointness checks when nonemptiness fails.

Assisted-by: Codex:GPT-6
2026-09-08 14:54:59 +02:00
Jechol Lee 360ed75d17 Mix test parameters into the per-test :rand seed (#15869)
ExUnit seeds each test process with {phash2(module), phash2(name), seed}.
parameterize re-runs the module per parameter set without changing the
test name, so every parameter set of a test started from the same :rand
state and drew the same random sequence.

Hash the parameters together with the name when they are present. Tests
without parameters keep their previous seed, so existing --seed
reproductions are unaffected.

Assisted-by: Claude Code:claude-fable-5-1

Signed-off-by: Jechol Lee <mr.jechol@gmail.com>
2026-09-08 13:11:39 +02:00
Alexander Gubarev 7f3b294e19 Fix kernel defstruct doc example (#15867) 2026-09-08 08:53:16 +09:00
José Valim 7427c0cf5d A binary may always be returned in a comprehension, closes #15857 2026-09-07 16:24:15 +02:00
Tobias Pfeiffer 0208ff88c3 Link the relevant docs from --partitions docs (#15865)
Very small docs improvement, thought it'd be nice if the section
was linked in the docs.

Built the docs and verified it works.
2026-09-07 11:13:55 +02:00
Alexander Gubarev a46b95b816 Fix IEx.h/1 macro spec introspection (#15863) 2026-09-07 08:26:05 +02:00
Alexander Gubarev 7223808989 Fix IEx incomplete codepoint autocomplete crash (#15864) 2026-09-07 08:25:50 +02:00
Barna Kovacs 24d5016632 Reuse existing tuple leaves when fusion fails (#15860) 2026-09-06 01:29:20 +02:00
Barna Kovacs 1d0094e3f8 Avoid duplicate arithmetic in Calendar.ISO.from_unix/2 (#15861)
Compute seconds and microseconds together with div_rem/2,
preserving floor division for negative timestamps.

Assisted-by: Codex:GPT-6
2026-09-06 00:43:04 +02:00
Barna Kovacs bb69e59b5c Correct tuple difference comments (#15859)
Size and content branches may overlap. The disjointness fast path leaves
the positive tuple unchanged.

Assisted-by: Codex:GPT-6
2026-09-05 21:03:41 +02:00
preciz 269b4a334b Detect disjoint maps with any required unmatched field
A required unmatched key cannot occur in the other closed map, even when
the remaining unmatched keys include optional fields. Recognize this
case early to avoid retaining unnecessary BDD nodes during subtraction.

Assisted-by: Codex:GPT-6
2026-09-05 18:42:08 +02:00
Łukasz Samson 695401e58b Fix __CALLER__.macro_aliases type (#15853)
Assisted-by: Codex:GPT-5
2026-09-05 11:50:37 +02:00
José Valim 2e9ce85e91 Fix precedence when parsing unary ops with do-end blocks
Closes #15849. Closes #15851.
2026-09-05 10:04:11 +02:00
Jean Klingler 4a74923ebb Minor improvements to invalid struct key error (#15852) 2026-09-05 16:48:42 +09:00
José Valim 48ff182db0 Also install if versions are different 2026-09-05 09:47:19 +02:00
Alexander Gubarev 6d4d45a268 Tighten macro unpipe/1 spec (#15850) 2026-09-05 09:44:30 +02:00
Barna Kovacs 82b1360aa4 Optimize Enum.min_max/1,2 for non-empty lists (#15845)
Assisted-by: Antigravity:Gemini-3.7-Flash
2026-09-05 00:38:34 +02:00
José Valim ccab980101 Fix swapped Hex version check condition, closes #15847 2026-09-05 00:37:27 +02:00
andresg-tsiglobal 6f635d4f12 Add missing 'end' to example in docs (#15846) 2026-09-04 20:28:21 +02:00
Barna Kovacs 3b5bcaa6d8 Clarify Keyword.drop/2 duplicate key behavior (#15844) 2026-09-04 13:19:14 +02:00
Alexander Gubarev b0d02d3965 Fix macro dbg/3 ignores ANSI color setting (#15842) 2026-09-04 08:34:50 +02:00
Barna Kovacs 96d47e12b2 Avoid list allocation in OptionParser when checking underscores (#15843)
Assisted-by: Antigravity:Gemini-3.8-Flash
2026-09-04 08:32:25 +02:00
Alexander Gubarev 394113faac Fix macro unescape_string/2 spec and doc (#15841) 2026-09-03 17:21:24 +02:00
Barna Kovacs bef6436cda Inline Calendar.ISO parsing heads and eliminate guarded helpers (#15840)
Pattern matching on "-" and "+" binary prefixes directly across the
public function heads of parse_date/2, parse_naive_datetime/2, and
parse_utc_datetime/2 eliminates the need for parse_date_guarded/2,
parse_naive_datetime_guarded/2, and parse_utc_datetime_guarded/2.

This removes an intermediate function dispatch on every date and
datetime parsing call and removes 9 lines of code.

Assisted-by: Antigravity:Gemini-3.7-Flash
2026-09-03 14:28:30 +02:00
Barna Kovacs fdebc8cba6 Optimize Date.Range.member?/2 by branching on step sign (#15838)
Branching on the step sign rather than `first_days <= last_days` eliminates
the need for `empty?/1` because the interval bounding checks inherently
guarantee non-emptiness. This aligns `Enumerable.Date.Range` with
`Enumerable.Range`, eliminates a private function call per membership
check, and removes 27 lines of redundant code.

Assisted-by: Antigravity:Gemini-3.7-Flash
2026-09-03 14:06:38 +02:00
Łukasz Samson a5cfa254a3 Fix unsound refinement of min/2 and max/2 arguments (#15835)
Do not refine both operands from the result expectation because either operand may be discarded by term ordering. Apply the expectation only to the result and cover escaping operand bindings.

Assisted-by: Codex:GPT-5
2026-09-03 10:08:45 +02:00
Jhan S. Álvarez aba75ca1b2 Add Macro.quoted_literal? clause for __block__ wrapped literals (#15836) 2026-09-03 09:29:08 +02:00
Alexander Gubarev d4b2c58979 Improve defstruct @enforce_keys (#15826) 2026-09-02 21:34:39 +02:00
Barna Kovacs 6fde3de13f Deduplicate gradual descriptor alignment (#15832) 2026-09-02 21:34:09 +02:00
Tobias Pfeiffer 936078255d Do not overlap sibling failures in parameterized tests failures manifest
Fixes #15820
2026-09-02 21:22:03 +02:00
Kuba Migdał 4d242a8c3d Sanitize ExUnit formatters against invalid UTF-8 (#15821) 2026-09-01 15:05:16 +02:00
Barna Kovacs a229c0e301 Optimize JSON map key encoding (#15824)
Inline the per-key dispatch to avoid a private function call for every encoded map entry.

Assisted-by: Codex:GPT-5
2026-09-01 09:34:37 +02:00
José Valim 87af1e34ec Do not check for emptiness in tuple element fallback 2026-09-01 01:05:04 +02:00
José Valim 3a21a6a50a Do not accept empty tuples in several tuple functions, closes #15817 2026-09-01 01:01:24 +02:00
Barna Kovacs 1962a66575 Avoid rebuilding Cartesian map suffixes (#15823) 2026-09-01 00:59:53 +02:00
Barna Kovacs 6a44ab621a Remove unused parameters in Myers list diff (#15822) 2026-09-01 00:59:35 +02:00
Alexander Gubarev c6d5bbcd9d Remove duplicate :elixir_def from Kernel.CLI @elixir_internals (#15819) 2026-08-31 18:02:28 +02:00
Alexander Gubarev 4fbdbc8acd Fix functions with impossible clauses typecheck crash (#15794) 2026-08-31 17:56:45 +02:00
Tyler A. Young 05576dd133 perf: Speed up Enum.dedup_by/2 on lists via a hand-rolled, tail-recursive implementation (#15815)
This improves the runtime of `Enum.dedup_by/2` on lists by replacting the generic `reduce/3` implementation with a hand-rolled version similar in spirit to the `dedup_list/1` optimization. In my benchmarking, the magnitude of the improvement depends heavily on the size and shape of the data, but it looks roughly like this:

| List length | Duplicates | Speedup |
|          10 |          0 |   +140% |
|       1,000 |          0 |    +81% |
|   1,000,000 |          0 |     +7% |
|          10 |        20% |   +116% |
|       1,000 |        20% |    +64% |
|   1,000,000 |        20% |     +2% |
|          10 |        50% |    +45% |
|       1,000 |        50% |    +62% |
|   1,000,000 |        50% |    +12% |
2026-08-31 17:50:38 +02:00
Alexander Gubarev 9e2f0ff348 Improve message for typespecs stepped ranges (#15816) 2026-08-31 17:32:19 +02:00
Alexander Gubarev c86e8d2b54 Fix Kernel.SpecialForms ::/2 doc example (#15818) 2026-08-31 17:31:39 +02:00
Alexander Gubarev 7bd7665a9b Fix DateTime convert/2 and truncate/2 ignoring custom calendar from map (#15814) 2026-08-31 15:54:35 +02:00
Rodolfo Carvalho 0ae594d716 Update Windows argument splitting warnings in Port and System (#15813)
Align the Windows argument splitting warning with Erlang/OTP's
open_port/2 documentation by including .cmd in the list of affected
extensions alongside .bat and .com, and fixing a missing final period.

Also polish the mitigation advice grammar to maintain parallelism
(double-checking).

Assisted-by: Antigravity:Gemini-3.7-Flash
2026-08-31 11:12:51 +02:00
Tiago Moraes dbd2fadba1 Simplify CaptureLog formatter test (#15810) 2026-08-30 19:06:32 +02:00
Jaideep Pyne a164205866 Add U+055F to the case ignorable set (#15812)
U+055F ARMENIAN ABBREVIATION MARK has Word_Break=MidLetter and is
Case_Ignorable in the UCD, but it was missing from the hardcoded list,
so the Greek final sigma context scan stopped at it instead of skipping
it. This affects String.downcase/2 and String.capitalize/2 in :greek
mode on both sides of the Final_Sigma rule.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: jaideeppyne <jaideeppyne1997@gmail.com>
2026-08-30 19:02:30 +02:00
Evan Purcer 26f6dbdb2a Docs updates for .ex/.exs plugin behavior (#15809) 2026-08-29 22:51:50 +02:00
Alexander Gubarev 0c8f1feb3a Remove unused task pid from from async stream internal state (#15806) 2026-08-28 19:54:01 +02:00
José Valim 1eff1acffd Fix recursion on charlist error path 2026-08-28 12:28:08 +02:00
Łukasz Samson dc939db4ad Refine map key checks for literal keys (#15804)
Assisted-by: Codex:GPT-5
2026-08-28 11:19:50 +02:00
Łukasz Samson b945cee1a9 Make singleton-typed pin patterns precise (#15805)
Assisted-by: Codex:GPT-5
2026-08-28 11:19:33 +02:00
Barna Kovacs 78f22cc8ef Refactor Enum.slide/3 list assembly (#15803)
Avoid concatenating at the end of lists.
2026-08-28 11:19:18 +02:00
José Valim 1d661fb91d Do not skip optional dependencies which are required downstream 2026-08-28 10:05:48 +02:00
José Valim 4147e50ea0 Update CHANGELOG 2026-08-27 09:44:19 +02:00
Łukasz Samson 5779f5be37 Widen stacktrace extra info types (#15798)
Match OTP's stacktrace contract in __STACKTRACE__ inference and the :erlang.raise/3 signature, allowing arbitrary keyword metadata.

Assisted-by: Codex:GPT-5.6
2026-08-27 09:37:44 +02:00
José Valim 3389f84059 Fix more warnings during test suite runs 2026-08-25 13:53:28 +02:00
Barna Kovacs 7d2a3d8a69 Reuse upper bound extraction in type descriptors (#15792)
Inline upper_bound/1 and replace Map.get(descr, :dynamic, descr) calls
with inlined pattern matching to eliminate function call overhead.

Assisted-by: Antigravity:Gemini-3.7-Flash
2026-08-24 17:59:24 +02:00
José Valim 21a1e47740 Fix warnings when running tests 2026-08-24 17:00:16 +02:00
Barna Kovacs 4bc288d47d Unify map domain type lookup (#15789)
Inline map_domain_tag_to_type/2 and remove the dead map_domain_tag_to_type/1 forwarding helper.

Assisted-by: Antigravity:Gemini-3.7-Flash
2026-08-24 16:27:11 +02:00
Barna Kovacs 2b2a99d0cf Avoid wrapping function arguments in phi_starter (#15788)
The return type was unused by disjoint_non_empty_domains?/3. Pass arguments directly to avoid tuple allocation.

Assisted-by: Antigravity:Gemini-3.7-Flash
2026-08-24 16:08:25 +02:00
Łukasz Samson e3d2cea83a Fix inferred type of :erlang.split_binary/2 result when arg is bitstring (#15790) 2026-08-24 16:06:02 +02:00
Alexander Gubarev 093d4c8873 Fix final sigma handling in :greek (#15791) 2026-08-24 16:05:49 +02:00
Barna Kovacs 628afe5b74 Avoid rebuilding list on failed element pop (#15787)
Return the updated list or :error directly.

Assisted-by: Antigravity:Gemini-3.7-Flash
2026-08-24 15:23:48 +02:00
Barna Kovacs 3d8b254724 Avoid rebuilding map domains during lookup (#15786)
map_get_domain/3 only consumes the selected domain value, but map_pop_domain_bdd/3 also rebuilt and returned the remaining map BDD. Use the existing domain lookup helper directly so negated map lookups do not construct state that is immediately discarded.

Assisted-by: Codex:GPT-5
2026-08-23 19:22:35 +02:00
Barna Kovacs 01ff4a5ec2 Reuse the top non-empty list descriptor (#15785)
Avoid rebuilding and hashing the canonical non-empty list top leaf when the tail is term().

Assisted-by: Codex:GPT-5
2026-08-23 18:21:10 +02:00
José Valim 63a7a054ce Remove none clauses if there are non empty clauses, closes #15777 2026-08-22 12:00:55 +02:00
José Valim 583150d002 Extract domain computation into a separate function 2026-08-22 12:00:55 +02:00
José Valim 6e05add26b Extract clause grouping into a separate function 2026-08-22 12:00:55 +02:00
Dino Kovač 4f0c820624 Add DateTime add/4 and shift_zone/3 UTC fast path (#15779) 2026-08-22 10:02:25 +02:00
Daniel KukulaandClaude Opus 5 11b7f08b0b Trim trailing whitespace without slicing per step (#15780)
trim_trailing/1 walked the string with binary_part/3, which allocated the
three byte lookahead plus a fresh prefix on every step even though every
prefix but the last one is discarded, and paid one allocation even when
there was nothing to trim.

Carry the position instead and slice once at the end. The six one-byte
whitespace codepoints, which are the common case, now need no lookahead at
all, so the lookahead tables only hold the multi-byte ones.

Dispatching on the byte with :binary.at/2 instead of matching the string is
what makes the common path free. Matching turns the argument into a match
context, and a clause returning the string unchanged then has to materialize
it again, which is the allocation we are trying to avoid.

Measured on OTP 29, ns/op and words allocated per call:

                          before        after
  100B, nothing to trim   47.6 / 8w    16.0 / 0w
  1KB, nothing to trim    47.4 / 8w    16.2 / 0w
  100B + one space        73.2 / 21w   41.4 / 5w
  100B + four spaces     104.4 / 34w   83.2 / 5w
  100B + NBSP             71.8 / 21w   60.9 / 13w
  100B + 64 spaces       754.8 / 294w 887.8 / 5w

Trimming every line of lib/elixir/lib/kernel.ex drops from 0.442ms to
0.134ms, and 2000 lines ending in a newline from 0.282ms to 0.094ms.

The trade-off is the last row. The old code advanced up to three bytes per
iteration through the lookahead table where this one advances one byte at a
time, so runs longer than about five whitespace bytes lose time, 15% to 20%
on a 64 byte run. Allocation on that run goes from O(n) to O(1), and long
trailing runs are rare next to lines ending in a single newline or in
nothing at all.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 00:57:08 +02:00
Thomas Cioppettini 30af9521cf Remove duplicate pattern compilation in Unicode data processing (#15776)
Assisted-by: Claude Code:claude-fable-5

Signed-off-by: Thomas Cioppettini <544875+tomciopp@users.noreply.github.com>
2026-08-21 09:14:19 +02:00
Daniel Kukula 5d605debe1 Fix base64 swar singleton false positive (#15773) 2026-08-21 09:13:59 +02:00
Hasan mohamed 345507aca9 Fix type checker crash on non-returning for ... into: expression (#15747) (#15770) 2026-08-20 22:39:18 +02:00
Thomas Cioppettini c4b42ddbbe Add unit fast paths to Calendar.ISO.iso_days_to_unit/2 (#15771)
Assisted-by: Claude Code:claude-fable-5

Signed-off-by: Thomas Cioppettini <544875+tomciopp@users.noreply.github.com>
2026-08-20 22:35:19 +02:00
Barna Kovacs 15572f54c1 Remove redundant list construction in String.replace (#15769) 2026-08-20 15:05:07 +02:00
José Valim fee29461cb Update docs logo 2026-08-20 14:20:40 +02:00
Daniel Kukula 4616a06f73 Optimize Macro.classify_atom (#15768)
Atoms made exclusively of ASCII letters, numbers and underscores, beginning
with a lowercase letter or underscore and optionally ending with ? or !, are
always identifiers, and only atoms prefixed by "Elixir" are candidates for
aliases. Recognizing both directly on the binary avoids building a charlist
and running the unicode aware identifier tokenizer, which dominates the cost
of classifying keyword list, map and struct keys when inspecting them.
2026-08-19 19:26:59 +02:00
Daniel Kukula 6cde8680ab Remove intermediate list in elixir_interpolation hex_to_int (#15767) 2026-08-18 17:36:38 +02:00
Thomas Cioppettini cb5fb8e2e0 perf: Copy file mode with chmod instead of stat and write_stat (#15754) 2026-08-18 17:26:11 +02:00
Daniel Kukula 11e920f9e4 Fast path for unescape_char/2 (#15766) 2026-08-18 17:25:48 +02:00
Jonatan Kłosko b703302cd1 Fix mix sync lock_N files getting overridden with empty content (#15765) 2026-08-18 13:25:11 +02:00
Thomas Cioppettini 02124013de Replace protocol dispatch with arithmetic in Access.slice/1 (#15764)
Assisted-by: Claude Code:claude-fable-5

Signed-off-by: Thomas Cioppettini <544875+tomciopp@users.noreply.github.com>
2026-08-16 21:24:13 +02:00
Barna Kovacs 11d2adf0ce Optimize Time.compare/2 (#15763) 2026-08-16 14:08:20 +02:00
Barna Kovacs c62216d7cb Optimize Date.compare/2 (#15762) 2026-08-16 13:27:02 +02:00
José Valim 9417808a56 Shift by unit in more occasions 2026-08-16 12:45:46 +02:00
Thomas Cioppettini 2db772efa6 perf: Add exact fast paths to Calendar.ISO.add_day_fraction_to_iso_days (#15759) 2026-08-16 12:24:12 +02:00
José Valim f6e177092e Update CONTRIBUTING.md 2026-08-16 10:06:17 +02:00
Thomas Cioppettini 82196d2bd9 fix: Range.disjoint?/2 for ranges beyond float precision (#15760) 2026-08-16 08:42:59 +02:00
Thomas Cioppettini 99b6970093 perf: Remove unused length computation in List.myers_difference (#15753) 2026-08-16 08:40:48 +02:00
Thomas Cioppettini f3a86d828b perf: Avoid intermediate charlist in String.downcase/upcase with :ascii (#15755) 2026-08-16 08:40:26 +02:00
Thomas Cioppettini 5b682995bf perf: Avoid copying the list in List.pop_at/3 for out-of-bounds indexes (#15756) 2026-08-16 08:28:59 +02:00
Thomas Cioppettini a6e7663f8f perf: Add accumulator free fast paths to Macro.prewalk/2 and postwalk/2 (#15758) 2026-08-16 08:27:52 +02:00
Thomas Cioppettini cf7e87a637 perf: Skip no-op microsecond step in Calendar.ISO shift functions (#15752) 2026-08-16 08:19:44 +02:00
dependabot[bot] e1738affb4 Bump azure/login from 3.0.0 to 3.0.1 (#15731)
Bumps [azure/login](https://github.com/azure/login) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/azure/login/releases)
- [Commits](https://github.com/azure/login/compare/532459ea530d8321f2fb9bb10d1e0bcf23869a43...f5d393ae46f8fde4be8b75f32e3fc50e654ad0ca)

---
updated-dependencies:
- dependency-name: azure/login
  dependency-version: 3.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-16 08:19:13 +02:00
dependabot[bot] e3327051f9 Bump DavidAnson/markdownlint-cli2-action from 24.1.0 to 24.2.0 (#15730)
Bumps [DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action) from 24.1.0 to 24.2.0.
- [Release notes](https://github.com/davidanson/markdownlint-cli2-action/releases)
- [Commits](https://github.com/davidanson/markdownlint-cli2-action/compare/6bf21b07787794f89a243495939cd651942aeabe...21c1be1b93ad9ed58fa840aacc3f279cde2a72ff)

---
updated-dependencies:
- dependency-name: DavidAnson/markdownlint-cli2-action
  dependency-version: 24.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-16 08:18:58 +02:00
dependabot[bot] f919b3e22b Bump zizmorcore/zizmor-action from 0.6.1 to 0.6.2 (#15728)
Bumps [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) from 0.6.1 to 0.6.2.
- [Release notes](https://github.com/zizmorcore/zizmor-action/releases)
- [Commits](https://github.com/zizmorcore/zizmor-action/compare/6fc4b006235f201fdab3722e17240ab420d580e5...3dc1ecc9bcb9e94e9b2c709687979e1298497054)

---
updated-dependencies:
- dependency-name: zizmorcore/zizmor-action
  dependency-version: 0.6.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-16 08:18:30 +02:00
Barna Kovacs a704e91133 Precompute ANSI escape sequences at compile time (#15749)
Assisted-by: Antigravity:Gemini-3.7-Flash
2026-08-14 17:38:18 +02:00
Eric Meadows-Jönsson a357d19110 Optimize comparison of parsed versions (#15748) 2026-08-14 07:06:30 -07:00
Jonatan Männchen 2096ac050d Update ORT & switch to faster license Scanner (#15746)
* Update ORT to v92.2.0

* Switch to Provenant-based scanner

Provenant is mostly compatible with ScanCode but a lot faster: in local
measurements the scan step went from 6:56 to 44s. Since Provenant is not
included in the ort-minimal image, the full ort image is used instead,
which slows down the analyzer step (1:08 to 1:44) due to the larger
image pull. Still a good reduction overall.
2026-08-14 14:48:38 +02:00
Thomas Cioppettini 7c4552c156 Avoid system_info call on every regex operation (#15745) 2026-08-14 13:16:24 +02:00
Alexander Gubarev bc7844c26c Fix Date.to_iso8601/2 loses :format with custom calendar (#15740) 2026-08-13 21:39:28 +02:00
Barna Kovacs f67cbf14a6 Optimize URI.append_query field access (#15741) 2026-08-13 21:03:00 +02:00
Andrea Leopardi a89dcdc865 Improve Enum.min_max* specs (#15739) 2026-08-13 10:44:35 +02:00
Alexander Gubarev c929e885bb Fix struct key malformed error message (#15735) 2026-08-12 23:46:03 +02:00
Barna Kovacs 9158e33883 Optimize EEx fn block detection (#15738)
Scan reversed tokens once to identify whether a stab expression starts an anonymous function block.

Assisted-by: Codex:GPT-5
2026-08-12 23:41:12 +02:00
Alexander Gubarev 7bf65189db Fix Task.yield_many/2 waits too long or forever when :limit exceeds task count (#15737) 2026-08-12 20:51:46 +02:00
Nelson Vides f316902a80 Optimize URI.encode_www_form/1 and RFC 3986 query encoding (#15736)
Both encoders were built on a binary comprehension whose body calls
`percent/2` through a captured predicate:

    for <<byte <- string>>, into: "" do
      case percent(byte, &char_unreserved?/1) do
        "%20" -> "+"
        percent -> percent
      end
    end

`for ... into: ""` over a binary generator compiles to a native Erlang
bitstring comprehension, so the comprehension itself is fine, but in the
body it pays per byte a `make_fun3` to build the captured predicate, a
call to `percent/2` plus an indirect call through it, a freshly
allocated one- or three-byte binary that is discarded immediately, an
`is_eq_exact` against the literal "%20", and a runtime-sized
`bs_create_bin` to append the result.

Rewrite it as a tail-recursive loop that writes the escape straight into
the accumulator. The unreserved test moves into a `defguardp` so it
inlines instead of going through a closure, `+` is handled by its own
clause rather than by comparing the produced binary against "%20", and
the appended segments have compile-time-known sizes.
2026-08-12 18:31:55 +02:00
José Valim b911bb0380 Raise when | is used in guards, closes #15734 2026-08-12 07:42:25 +02:00
José Valim 37c7da4fbf Fix constant retrigger of protocol consolidations in umbrellas
Closes #15727.
2026-08-11 22:09:55 +02:00
José Valim 6ef6f78206 Properly escape unicode codepoints above ASCII range 2026-08-11 17:10:23 +02:00
José Valim 024beb1bfc Update design-anti-patterns.md 2026-08-10 14:21:53 +02:00
Mikhail Nekrashevich 44085d4347 Escape C1 control characters with Unicode escapes (#15726)
Assisted-by: Codex:GPT-5
2026-08-09 09:21:22 +02:00
makoto-developer 0bd52d4a7e Only use do-end blocks when every trailing keyword is a block keyword (#15725) 2026-08-08 09:05:26 +02:00
Daniel Kukula 48c515f994 Remove accumulator from parse_digits, reuse all_digits? function (#15722) 2026-08-07 20:23:14 +02:00
Daniel Kukula 81fb10eec8 Remove accumulator from parse_microsecond, use binary_part (#15723) 2026-08-07 20:22:11 +02:00
Alexander Gubarev 8b584147e8 Validate microseconds in Time.from_seconds_after_midnight/3 (#15721) 2026-08-07 18:09:37 +02:00
Jonatan Männchen 5e7cb9a707 Add Path.safe_join/2 (#15720) 2026-08-07 15:57:22 +02:00
José Valim 2971952edc Fix escaping of literal interpolated strings, closes #15718 2026-08-07 12:28:51 +02:00
José Valim 309bb8a875 Unpack guard operations from all modules 2026-08-07 12:28:51 +02:00
Daniel Kukula e697189e3b Remove intermediate list in float parse (#15719) 2026-08-07 10:33:00 +02:00
dependabot[bot] 842faf0ec5 Bump zizmorcore/zizmor-action from 0.6.0 to 0.6.1 (#15715)
Bumps [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) from 0.6.0 to 0.6.1.
- [Release notes](https://github.com/zizmorcore/zizmor-action/releases)
- [Commits](https://github.com/zizmorcore/zizmor-action/compare/6599ee8b7a49aef6a770f63d261d214911a7ce02...6fc4b006235f201fdab3722e17240ab420d580e5)

---
updated-dependencies:
- dependency-name: zizmorcore/zizmor-action
  dependency-version: 0.6.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-05 09:50:14 +02:00
José Valim 63e5a53925 Update CHANGELOG 2026-08-04 15:44:49 +02:00
makoto-developer 79cf2ffa15 Fix System.stacktrace arity in the table of deprecations (#15713) 2026-08-04 08:34:01 +02:00
José Valim 6c927126bb Add LICENSE info to ORT template 2026-08-03 11:18:54 +02:00
José Valim 7df894e181 Ignore github files on markdown lint 2026-08-03 10:48:00 +02:00
José Valim 7ac3ec1a39 Create pull request template 2026-08-03 10:45:09 +02:00
makoto-developer d5f4d491c9 Fix Kernel.SpecialForms references in Macro.Env docs (#15712) 2026-08-03 10:38:50 +02:00
Rodolfo Carvalho 44405c300b Ensure no double newlines in files generated by "mix new" (#15711) 2026-08-03 08:59:29 +02:00
Rodolfo Carvalho a9021b1c8d Fix trailing whitespace in files generated by "mix new" (#15710) 2026-08-02 11:22:48 +02:00
Daniel Kukula 25bba6c57e Use tail recursion in tuple aggregates (#15708) 2026-08-01 21:54:16 +02:00
Barna Kovacs 8c49052a41 Optimize Calendar.ISO.parse_duration/1 (#15709)
Apply the leading duration sign as components are parsed, avoiding a second pass for negative durations.

Track allowed date and time units with ordered positions instead of keyword-list tails and recursive lookups.

Assisted-by: Codex:GPT-5
2026-08-01 21:43:46 +02:00
Barna Kovacs dc1ac5f461 Remove redundant pending clause check (#15707) 2026-07-31 16:54:07 +02:00
Barna Kovacs 03dda70bdd Optimize key lookups in Macro (#15706)
Use fetch operations to avoid looking up keyword and map keys twice.

Assisted-by: Codex:GPT-5
2026-07-31 15:40:42 +02:00
Barna Kovacs 1a711f95d3 Fix typespecs for scoped macro counters (#15705) 2026-07-30 21:38:01 +02:00
Alexander Gubarev df537c4d44 Fix Calendar.strftime/3 perfomance regression (#15704) 2026-07-30 17:04:53 +02:00
Alexander Gubarev a46b6e3ade Fix Code.Fragment specs and docs (#15701) 2026-07-30 12:34:48 +02:00
Alexander Gubarev ca29070564 Fix Code doc wordings and clarify (#15703) 2026-07-30 12:29:50 +02:00
Alexander Gubarev a0b17a355a Fix Code.Typespec.spec_to_quoted/2 incomplete variable type collection (#15700) 2026-07-29 19:17:07 +02:00
Alexander Gubarev a5e437e2cd Fix Code specs and docs (#15699) 2026-07-29 15:26:48 +02:00
dependabot[bot] 4434e1f5ea Bump zizmorcore/zizmor-action from 0.5.7 to 0.6.0 (#15696)
Bumps [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) from 0.5.7 to 0.6.0.
- [Release notes](https://github.com/zizmorcore/zizmor-action/releases)
- [Commits](https://github.com/zizmorcore/zizmor-action/compare/192e21d79ab29983730a13d1382995c2307fbcaa...6599ee8b7a49aef6a770f63d261d214911a7ce02)

---
updated-dependencies:
- dependency-name: zizmorcore/zizmor-action
  dependency-version: 0.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-29 09:56:15 +02:00
dependabot[bot] 793583d057 Bump DavidAnson/markdownlint-cli2-action from 24.0.0 to 24.1.0 (#15697)
Bumps [DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action) from 24.0.0 to 24.1.0.
- [Release notes](https://github.com/davidanson/markdownlint-cli2-action/releases)
- [Commits](https://github.com/davidanson/markdownlint-cli2-action/compare/8de2aa07cae85fd17c0b35642db70cf5495f1d25...6bf21b07787794f89a243495939cd651942aeabe)

---
updated-dependencies:
- dependency-name: DavidAnson/markdownlint-cli2-action
  dependency-version: 24.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-29 09:55:46 +02:00
dependabot[bot] 2d60a959c0 Bump actions/checkout from 7.0.0 to 7.0.1 (#15698)
Bumps [actions/checkout](https://github.com/actions/checkout) from 7.0.0 to 7.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0...3d3c42e5aac5ba805825da76410c181273ba90b1)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-29 09:55:25 +02:00
Alexander Gubarev 2d0e5bab83 Fix Date.range/3 raises wrong exception for invalid step (#15690) 2026-07-28 13:51:46 +02:00
José Valim 8425a039f2 Avoid double comparisons in predicate functions 2026-07-28 13:39:44 +02:00
Alexander Gubarev 138680489e Fix IEx.Helpers.h print spec for default arg function (#15692) 2026-07-28 13:23:40 +02:00
Alexander Gubarev 550b01db0d Fix Code.Formatter typos (#15691) 2026-07-28 10:41:47 +02:00
José Valim e7a03d2feb Reduce allocations in the logger translator module 2026-07-28 10:39:54 +02:00
José Valim 8a1bbb59df Warn on errors from orphan compiler modules 2026-07-28 10:19:21 +02:00
Guillaume Duboc 745ee1d538 Fix none handling + map fetch rewrite (#15686) 2026-07-27 13:59:08 +02:00
José Valim ce47442332 Remove Erlang Syntax crash course link
Closes #15680.
2026-07-27 10:08:55 +02:00
Alexander Gubarev 6c413a893b Fix Calendar docs wordings and clarify (#15684) 2026-07-27 09:47:52 +02:00
Alexander Gubarev 6ef392a98d Add specs to Kernel.ParallelCompiler.pmap (#15683) 2026-07-27 09:47:19 +02:00
Alexander Gubarev 2dcba2c0cb Add specs to Config.config and Config.read_config (#15682) 2026-07-27 09:47:00 +02:00
Alexander Gubarev 9f490a187f Add specs to Macro.compile_apply (#15681) 2026-07-27 09:45:32 +02:00
Benjamin Milde 7c836e221a Remove trailing comma from doctest (#15679) 2026-07-26 18:44:37 +02:00
Alexander Gubarev a598106b23 Fix 'not in' as continuation operator in IEx (#15678) 2026-07-26 14:37:16 +02:00
Alexander Gubarev 4b4ac32b73 Remove stale :three_op operator from IEx.Evaluator (#15677) 2026-07-25 21:40:37 +02:00
Łukasz Samson 9e78534880 Do not return {:ok, _} on maps with empty lines in map_put_static_value (#15597) 2026-07-25 18:29:08 +02:00
Barna Kovacs b8c1eb7de2 Defer function application computations until needed (#15676) 2026-07-25 18:28:58 +02:00
Barna Kovacs 4e616904c1 Remove redundant abs calls from Calendar.ISO (#15675)
The BCE guards establish the sign of both calculations, so compute their positive magnitudes directly.

Assisted-by: Codex:GPT-5
2026-07-25 17:33:20 +02:00
Aaron Tinio 44ea7a621e Pass :sigils to mix format plugins invoked for a sigil (#15672)
mix format passes the :sigils option to a plugin invoked for a file
extension, but not to a plugin invoked for a sigil, because the sigil
formatter closure captures the options before :sigils is put into them.
A plugin that formats embedded Elixir code therefore cannot dispatch a
sigil nested inside the sigil it is formatting by passing on the options
it received, so the same snippet formats differently in a .heex file and
in an ~H sigil.

The sigil formatters are part of the options they receive, so a private
function rebuilds the list whenever a sigil is formatted.
2026-07-25 10:52:31 +02:00
Jean Klingler a8d9921a89 Add admonition block about MapSet opaqueness issue (#15674) 2026-07-25 17:01:20 +09:00
Barna Kovacs 82e17e8bc3 Remove redundant abs call from Date.Range (#15669)
The preceding clauses handle steps that point away from the last date.
Otherwise, `last_days - first_days` and `step` have the same sign,
so `div/2` is already non-negative.
2026-07-25 09:40:15 +02:00
Łukasz Samson 17f782e0eb Fix tuple_insert_static crash on non-normalized empty type (#15542)
Closes #15541.
2026-07-25 09:38:44 +02:00
Barna Kovacs 6fda428a21 Remove redundant min from Enum.drop/2 (#15671)
The negative amount guard guarantees amount + count is always less than count.

Assisted-by: Codex:GPT-5
2026-07-25 09:23:06 +02:00
Guillaume Duboc e303c78201 Rework tuple projection algorithm (#15668) 2026-07-25 09:14:30 +02:00
Renan Ribeiro 0f832059da Fix typo in keyword module documentation (#15670) 2026-07-25 09:12:54 +02:00
Alexander Gubarev 3b96b74a51 Add specs to Kernel.binary_slice (#15667) 2026-07-24 17:51:49 +02:00
Barna Kovacs 70624eff47 Remove redundant abs calls from Range (#15666) 2026-07-24 17:10:30 +02:00
Alexander Gubarev ffcea82a1e Fix Calendar.strftime/3 width calculations (#15665) 2026-07-24 09:29:25 +02:00
Alexander Gubarev a8f18d8105 Fix Kernel.ParallelCompiler.compile_opts() type each_cycle spec (#15664) 2026-07-24 08:37:10 +02:00
Alexander Gubarev aaaa921781 Fix pages code examples (#15663) 2026-07-24 08:32:58 +02:00
Alexander Gubarev e20ca7e788 Clarify ExUnit --slowest-modules switch header (#15660) 2026-07-23 18:36:26 +02:00
Alexander Gubarev e308336c94 Fix Calendar.strftime negative year as 2-digits formatting (#15658) 2026-07-23 18:36:19 +02:00
Alexander Gubarev f08dff3536 Fix ExUnit.RunnerStats.increment_failure_counter/2 spec (#15659) 2026-07-23 18:20:47 +02:00
Alexander Gubarev 8f63ee11b3 Fix IEx.configure_opts() typespec and add missing IEx.Server doc option (#15661) 2026-07-23 18:17:32 +02:00
José Valim 5d2ac0ca69 Make monitored test check more reliable 2026-07-23 09:00:30 +02:00
Alexander Gubarev 6e57f00e4b Fix Calendar.ISO.valid_time? ms_precision guard (#15655) 2026-07-23 08:37:45 +02:00
Barna Kovacs 1570992d04 Optimize stale dependency module lookups (#15656)
Reuse the manifest modules map instead of building a list and
scanning that list for every dependency export.
2026-07-23 08:36:40 +02:00
Alexander Gubarev e713dc9c9b Fix kernel docs wordings and clarify (#15654) 2026-07-22 19:39:25 +02:00
Alexander Gubarev 8eb51e6494 Fix Calendar.ISO self-referencing doc in naive_datetime_to_iodata, datetime_to_iodata (#15653) 2026-07-22 17:40:07 +02:00
Alexander Gubarev e641683a41 Fix Calendar.ISO.parse_time negative zero offsets in basic format (#15651) 2026-07-22 15:53:38 +02:00
Alexander Gubarev 4270828c93 Fix ParallelCompiler doc wordings and typos (#15652) 2026-07-22 15:43:10 +02:00
nseaprotector 002e42d574 Fix diff/3 rounding for :day, :hour and :minute (#15650)
The `:day`, `:hour` and `:minute` clauses of `Time.diff/3`,
`DateTime.diff/3` and `NaiveDateTime.diff/3` compute the difference in
seconds first and then divide again, so the sub-second remainder is
rounded twice. The intermediate conversion floors while the final
`div/2` truncates, so the result follows neither rule:

    # elapsed is 23:59:59.999998, less than a full day
    Time.diff(~T[00:00:00.000001], ~T[23:59:59.999999], :hour)
    #=> -24, while the opposite direction returns 23

    # elapsed is 86399.5s, and the docs promise incomplete days round to zero
    NaiveDateTime.diff(~N[2000-01-01 00:00:00.7], ~N[2000-01-02 00:00:00.2], :day)
    #=> -1

Compute the difference in microseconds and divide once instead. This
keeps the documented truncation ("fractional results are not supported
and are truncated", "it also rounds incomplete days to zero") in both
directions. Positive results are unchanged, so the existing
`DateTime.diff/3` "almost 7 days" test now gains its negative
counterpart.

Assisted-by: Claude Code:claude-opus-4-8
2026-07-22 13:09:34 +02:00
Alexander Gubarev e94774926b Fix elixirc --profile time switch argv parsing (#15647) 2026-07-21 21:18:49 +02:00
Alexander Gubarev 7918013cfd Fix String.reverse/1 grapheme ordering around invalid utf8 bytes (#15645) 2026-07-21 20:48:24 +02:00
Łukasz Samson fa7db38e6f Improve send return type (#15644)
Teach the type checker that Kernel.send/2 returns the message operand.
2026-07-21 20:32:02 +02:00
José Valim 1a63a83e07 Improve String synchronization docs 2026-07-21 19:35:40 +02:00
Frank Hunleth cff8010e80 Fix System.cmd/3 unit test that fails on Ubuntu 26.04 (#15646)
This test runs `System.cmd/3` against a symlink to the `echo` command.
This works when `echo` is its own binary and when it's in a multicall
binary that's dispatched using `argv[0]`. It doesn't work for the new
Rust/uutils coreutils which doesn't dispatch via `argv[0]`. In this
case, intended call to echo produces no output:

```
  1) test Unix cmd/3 with absolute and relative paths (SystemTest)
     test/elixir/system_test.exs:210
     match (=) failed
     code:  assert {"hello\n", 0} = System.cmd(Path.join(File.cwd!(), @echo), ["hello"], arg0: "echo")
     left:  {"hello\n", 0}
     right: {"", 0}
     stacktrace:
       test/elixir/system_test.exs:223: anonymous fn/0 in SystemTest."test Unix cmd/3 with absolute and relative paths"/1
       (elixir 1.21.0-dev) lib/file.ex:2100: File.cd!/2
       test/elixir/system_test.exs:215: (test)
```

This fix switches the binary used for the test over to `sh` which fixes
the issue on Ubuntu 26.04. This was also verified on Ubuntu 24.04,
Ubuntu 22.04, Alpine Linux 3.24, macOS Tahoe (26.5.1) and MacOS Sequoia
(15.3.2).
2026-07-21 18:42:15 +02:00
José Valim 369f9b8844 Remove CHANGELOG entry on private API 2026-07-20 23:59:58 +02:00
Alexander Gubarev dbf74a6098 Fix Registry doc and specs (#15640) 2026-07-20 23:37:05 +02:00
Barna Kovacs 450a24c840 Optimize Range.disjoint?/2 empty checks (#15639) 2026-07-20 18:14:03 +02:00
Alexander Gubarev ba04ada65d Fix StringIO.open/2 doc (#15638) 2026-07-19 20:08:07 +02:00
Barna Kovacs e25f52019a Optimize disjoint function argument checks (#15637)
Avoid allocating a zipped list before checking whether
function domains are disjoint.
2026-07-19 16:59:13 +02:00
José Valim 8ea2a5dc17 Update CHANGELOG 2026-07-19 13:32:40 +02:00
José Valim 85b5838020 Ensure %_{} is precise in guards, closes #15635 2026-07-19 10:05:38 +02:00
Alexander Gubarev f16d8d290f Fix ExUnit configure_opts swapped doc defaults (#15634) 2026-07-18 21:50:55 +02:00
Alexander Gubarev 6689fc1ff4 Fix ExUnit.EventManager calling Supervisor on DynamicSupervisor (#15633) 2026-07-18 17:36:11 +02:00
LJZN 34abdd4361 Optimize Inspect.List.keyword?/1 by using Atom.to_string/1 (#15631)
Atom.to_charlist/1 allocates a new list per atom key when checking
if a keyword list contains module names. Atom.to_string/1 produces
a binary that can be pattern matched directly, avoiding O(n) temporary
allocations when inspecting large maps and keyword lists.

Benchmark (n=10):
  keyword?/1 (10K):  0.62ms -> 0.48ms (1.3x)
  keyword?/1 (50K):  2.72ms -> 1.69ms (1.6x)
  inspect map (10K): 2.59ms -> 1.52ms (1.7x)
  inspect map (50K): 8.85ms -> 5.94ms (1.5x)

Assisted-by: opencode:deepseek-v4-flash
2026-07-18 12:35:49 +02:00
José Valim 9a6a7b38a8 Include adversarial reviews on compiler and type system work 2026-07-17 22:21:10 +02:00
José Valim d7c0671877 Bring back open_api_spex optimization 2026-07-17 21:10:58 +02:00
José Valim 27438994fd Optimize equal-head BDD intersections
Only preserve a shared union when absorption removes all
residual union terms. Normalize other equal-head intersections
eagerly to prevent exponential growth.

Closes #15629.

Assisted-by: Codex:GPT-5
2026-07-17 19:37:10 +02:00
Alexander Gubarev 92648c7eb2 Fix IEx.Helpers.process_info/1 heap and stack memory calculation (#15628) 2026-07-17 17:30:24 +02:00
Alexander Gubarev dae2bcb4bd Fix IEx.Helpers.r/1 fails reload modules defined in same file (#15627) 2026-07-17 16:22:34 +02:00
José Valim 7bb87a7d0a Improve coverage of defguards 2026-07-17 16:21:50 +02:00
José Valim 62e5ebbda9 Tidy up language description 2026-07-17 11:55:00 +02:00
José Valim 751a518d06 Fix Elixir logo image source in README
Updated the image source path for the Elixir logo.
2026-07-17 11:54:13 +02:00
José Valim 895b4064c9 Remove :pid from info assertions 2026-07-17 10:35:22 +02:00
José Valim 0a21418534 Underscore unused variable 2026-07-17 10:12:29 +02:00
José Valim ca719e08fb Do not store optional in descr (#15624) 2026-07-17 01:20:39 +02:00
José Valim 2e2fdf84c8 Fix tests on Erlang/OTP main 2026-07-17 01:20:13 +02:00
José Valim 8564fd506a Remove recursive list intersection (#15622)
We couldn't measure any performance
benefit after latest optimizations.
2026-07-16 21:27:36 +02:00
José Valim 59e257d3ca Optimize tuple and map emptiness checks (#15619)
Short-circuit disjoint and subtype tuple elements
before exploring the general difference branches.

Assisted-by: Codex:GPT-5.6
2026-07-16 21:27:06 +02:00
Alexander Gubarev 51ac6a5c0f Fix refute_in_delta boundary logic and negative delta (#15623) 2026-07-16 19:09:17 +02:00
Łukasz Samson 0821930417 Split tuple_insert_static into two paths (#15518)
- bdd_map on pure positive disjunction
- expand to the exact negation-free positive DNF, insert and union on negations and/or implicit :bdd_top

Fixes #15517
2026-07-16 17:49:13 +02:00
Patric f9a7552ab5 Fix incorrect examples and typos in guides (#15620)
The guide pages under lib/elixir/pages are not doctests, so some
example outputs drifted from what current Elixir prints. I re-ran
each example on main (1.21.0-dev, OTP 29) with a clean IEx and
updated the blocks to match the actual output. Also fixes two typos.
Map ordering examples were left untouched since small-map print
order is not guaranteed across OTP versions.

Signed-off-by: Patric Vinicios <psdev7@gmail.com>
2026-07-16 17:29:46 +02:00
Alexander Gubarev f2ad598ca3 Fix ExUnit StringIO processes leak when named device capture failed (#15621) 2026-07-16 17:29:21 +02:00
Alexander Gubarev 1bfa86baf3 Fix ExUnit doc wordings and func links (#15618) 2026-07-16 15:08:49 +02:00
José Valim 4720b39284 Revert "Bump github/codeql-action/analyze and /init from 4.36.2 to 4.36.3 (#15613, #15614)"
This reverts commit 7571ff2672.
This reverts commit 58b62fb594.
2026-07-16 13:58:05 +02:00
José Valim 4fbe8ee9a7 Optimize empty descr keys 2026-07-16 13:53:42 +02:00
Guillaume Duboc d0b5519700 Support recursive type descriptors (#15530) 2026-07-16 13:36:17 +02:00
Alexander Gubarev 84f3ed66c9 Fix IEx.Helpers.i/1 doc example to actual shape (#15617) 2026-07-15 12:25:58 +02:00
dependabot[bot] 7571ff2672 Bump github/codeql-action/init from 4.36.2 to 4.36.3 (#15614)
Bumps [github/codeql-action/init](https://github.com/github/codeql-action) from 4.36.2 to 4.36.3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/8aad20d150bbac5944a9f9d289da16a4b0d87c1e...54f647b7e1bb85c95cddabcd46b0c578ec92bc1a)

---
updated-dependencies:
- dependency-name: github/codeql-action/init
  dependency-version: 4.36.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-15 10:13:30 +02:00
dependabot[bot] 58b62fb594 Bump github/codeql-action/analyze from 4.36.2 to 4.36.3 (#15613)
Bumps [github/codeql-action/analyze](https://github.com/github/codeql-action) from 4.36.2 to 4.36.3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/8aad20d150bbac5944a9f9d289da16a4b0d87c1e...54f647b7e1bb85c95cddabcd46b0c578ec92bc1a)

---
updated-dependencies:
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.36.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-15 10:12:55 +02:00
dependabot[bot] 0676818543 Bump DavidAnson/markdownlint-cli2-action from 23.2.0 to 24.0.0 (#15615)
Bumps [DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action) from 23.2.0 to 24.0.0.
- [Release notes](https://github.com/davidanson/markdownlint-cli2-action/releases)
- [Commits](https://github.com/davidanson/markdownlint-cli2-action/compare/ded1f9488f68a970bc66ea5619e13e9b52e601cd...8de2aa07cae85fd17c0b35642db70cf5495f1d25)

---
updated-dependencies:
- dependency-name: DavidAnson/markdownlint-cli2-action
  dependency-version: 24.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-15 10:11:17 +02:00
José Valim c423e83fb0 Clarify formatter docs, closes #15616 2026-07-15 09:55:47 +02:00
Alexander Gubarev 7e9c89780c Fix IEx completion crash on maps with non-atom keys (#15612) 2026-07-14 21:55:56 +02:00
Panagiotis Nezis 3cf8682687 Fix Duration specs to allow negative microsecond values (#15611)
`Duration.t` and `Duration.unit_pair` referenced `Calendar.microsecond()`,
whose value is a `non_neg_integer`. However, durations may represent
negative amounts of time, negative microsecond values are advertised
by the module documentation, produced by parsing, and returned by the
module's own doctests:

    Duration.from_iso8601!("PT-0.5S")
    #=> %Duration{microsecond: {-500000, 1}}

Introduce a dedicated `Duration.microsecond()` type whose value is an
integer and whose precision is 0..6, matching what `new!/1` validates.
2026-07-14 18:08:30 +02:00
Alexander Gubarev d1a33f8b63 Fix Map.from_struct/1 typecheck (#15610) 2026-07-14 16:51:19 +02:00
Alexander Gubarev 2429df6a6a Fix IEx missing evaluator continuation binary operator ** (#15608) 2026-07-14 13:00:14 +02:00
Alexander Gubarev 1db0a0c3fd Fix EEx.tokenize/2 missing comment token in result doc (#15605) 2026-07-14 11:29:44 +02:00
Panagiotis Nezis 92c3b9fddd Fix Enum.min/2,3 and Enum.max/2,3 with a custom sorter on ranges (#15603)
The range clause of `aggregate/3` only compared the two endpoints of the
range, which is correct solely for sorters consistent with the natural
integer order. Any other comparator returned a different result than
the identical call on the equivalent list or stream:

    sorter = fn a, b -> rem(a, 5) >= rem(b, 5) end
    Enum.max(1..10, sorter)               #=> 1, expected 4
    Enum.max(Enum.to_list(1..10), sorter) #=> 4

Once the range is known to be non-empty, take the endpoint shortcut
only when the sorter is one of the defaults (`&>=/2` or `&<=/2`, both
order-consistent). Any other sorter traverses the elements through
`reduce_range/5`, seeded with the first element.
2026-07-14 11:19:08 +02:00
Alexander Gubarev 9ed6724440 Update Process.flag/2 specs (#15602) 2026-07-14 11:02:52 +02:00
Panagiotis Nezis 4c42f94aa2 Delete duplicate keys on :pop in Keyword.get_and_update/3 and get_and_update!/3 (#15604)
Both functions document updating "in one pass, deleting duplicate keys",
but the `:pop` branch left any later duplicates in place:

    Keyword.get_and_update([a: 1, a: 2], :a, fn _ -> :pop end)
    #=> {1, [a: 2]}, expected {1, []}
2026-07-14 10:51:11 +02:00
Alexander Gubarev 025d8ef8ce Fix Process.dest() spec and Process.send/3 update docs (#15601) 2026-07-13 19:26:33 +02:00
Alexander Gubarev e578a71b79 Fix IEx.Info.info/1 spec and update docs (#15600) 2026-07-13 00:32:29 +02:00
Alexander Gubarev 1a4a91a1f3 Fix EEx.eval_string/3 and EEx.eval_file/3 specs (#15599) 2026-07-12 17:51:23 +02:00
Alexander Gubarev ac8c3dd3ab Fix IEx.__break__!/3 fallback clause arity (#15595) 2026-07-12 12:32:08 +02:00
José Valim 6adff3fcc2 Fix cursor completion after operators
Treat trailing operator keywords as completion fragments
when they follow another operator.

Assisted-by: Codex:GPT-5

Closes #15591.
2026-07-11 16:52:00 +02:00
José Valim 259d52cba7 Provide proper signature for ++
And deal with compatibility checks in to_existing_atom/2.

Closes #15588.
2026-07-11 11:12:15 +02:00
nseaprotector 32597f08f7 Clarify :allow_pre does not affect < and <= in Version docs (#15590)
The `Version` moduledoc and `Version.match?/3` documentation stated
unconditionally that, with `allow_pre: false`, a pre-release version
would not match unless the operand was itself a pre-release. This holds
for the `~>`, `>`, and `>=` operators, which carry the `allow_pre`
guard, but not for `<` and `<=`, which have no such guard and match
pre-releases according to precedence regardless of the option:

    Version.match?("1.9.0-rc", ">= 1.0.0", allow_pre: false)  # => false
    Version.match?("1.9.0-rc", "< 2.0.0",  allow_pre: false)  # => true
2026-07-11 08:58:45 +02:00
Panagiotis Nezis a3522e4a25 Return 1.0 from String.bag_distance/2 with empty strings (#15587)
Both `String.jaro_distance/2` and the general bag distance algorithm return
1.0 for equal strings; the empty pair was the only exception.
2026-07-10 23:11:47 +02:00
Panagiotis Nezis 8fcf677082 Reject integer 0 in Float.round/2 (#15585) 2026-07-10 23:02:32 +02:00
Alexander Gubarev 6b61234adb Registry :compressed type opt and doc (#15586) 2026-07-10 21:40:36 +02:00
Panagiotis Nezis 717952aacf Reject non-integer arguments in Integer.extended_gcd/2 zero clauses (#15584)
The zero base-case clauses guarded only on comparison with 0, so under
Erlang term ordering non-integer arguments (floats, atoms) matched when
the other argument was 0, e.g. `Integer.extended_gcd(2.5, 0)` returned
`{2.5, 1, 0}` instead of raising, violating the spec and diverging from
`Integer.gcd/2`.
2026-07-10 19:46:23 +02:00
nseaprotector 5cad659e6d Fix NaiveDateTime.diff/3 over-counting incomplete units (#15583) 2026-07-10 18:19:18 +02:00
nseaprotector b3e4738a4c Fix Range.disjoint?/2 for single-element ranges with a negative step (#15582) 2026-07-10 13:04:10 +02:00
Alexander Gubarev f54719040c Improve docs for List.to_unsafe_atom/1 and String.to_unsafe_atom/1 (#15576) 2026-07-10 09:59:51 +02:00
Łukasz Samson f3aab1140b Add missing calendar key to Calendar.time() and fix truncate spec (#15572) 2026-07-10 09:51:13 +02:00
Łukasz Samson 5b21c6a168 Fix List specs that are broader than the inputs handled (#15571) 2026-07-10 09:49:32 +02:00
Łukasz Samson 48c124abbe Accept chardata paths in Mix.Release.make_boot_script/4 (#15573)
Found by re-running the type checker over function bodies with
spec-derived argument domains (#15559): the path argument is spec'd
Path.t() (chardata), but the body applies <> and String.to_charlist/1
to it, so a charlist path -- valid per the spec -- raised
ArgumentError. Normalize with IO.chardata_to_string/1 first, as the
Path module functions do.
2026-07-10 09:45:04 +02:00
Alexander Gubarev b7a82bfcea Add spec to Registry.lock/3 (#15575) 2026-07-10 09:42:07 +02:00
Alexander Gubarev d0fb26a68a Add missing result marker in Inspect moduledoc (#15577) 2026-07-10 09:41:21 +02:00
Alexander Gubarev a324c883ce Fix Duration.from_iso8601/1 accepting duplicate S (secs) when fractional 2026-07-10 09:40:53 +02:00
Alexander Gubarev f401038c31 Expand Calendar examples and indent paragraph (#15578) 2026-07-10 09:36:10 +02:00
Alexander Gubarev 6d12dcecf7 Clarify pop_lazy docs (#15580) 2026-07-10 09:35:35 +02:00
nseaprotector e746c042bb Refine Time.to_seconds_after_midnight/1 return type (#15570) 2026-07-09 22:36:37 +02:00
nseaprotector 4e840bc094 Fix Agent timeout docs: non-negative, not greater than zero (#15569) 2026-07-09 20:39:38 +02:00
nseaprotector 345deb35cc Align GenServer.call/3 timeout doc to specs (#15568) 2026-07-09 20:38:57 +02:00
nseaprotector b7e22baa45 Fix Calendar.ISO.valid_time?/4 doc (#15567) 2026-07-09 20:38:07 +02:00
nseaprotector 1d9d560fcd Remove inaccurate note from Node.set_cookie/2 doc (#15566) 2026-07-09 20:24:05 +02:00
nseaprotector 6957e15210 Make IO.getn and IO.read docs consistent (#15565) 2026-07-09 20:01:30 +02:00
Alexander Gubarev b82c44a109 Update most common delimeters in Markdown ANSI rendering (#15564) 2026-07-09 16:50:40 +02:00
Łukasz Samson 1cb2089367 Tighten stdlib typespecs to match implementations (#15561)
Candidates found by cross-checking @specs against the type signatures
inferred by the compiler (see #15559): these specs declared strictly
wider return types than the functions can produce, where the gap
carries no intentional API room (deliberately abstract contracts, such
as atom() covering future values, are left untouched):

  * File.Stream.t/0: type the struct fields (mirrors IO.Stream.t/0)
  * Map.new/0: %{} instead of map()
  * Logger.levels/0: Exclude deprecated :warn
  * Macro.Env.location/1 and Macro.Env.stacktrace/1: precise shapes
2026-07-09 16:50:13 +02:00
Victor Rodrigues c7586755a6 Document --failed interaction with --partitions in mix test (#15563) 2026-07-09 14:27:14 +02:00
Łukasz Samson 77426cc770 Handle Erlang/OTP 28 nominal types in Code.Typespec.fetch_types/1 (#15562) 2026-07-09 14:25:59 +02:00
Alexander Gubarev ecf55172c7 Fix DateRange slice step computation (#15560) 2026-07-09 13:46:23 +02:00
nseaprotector 251d3fe5b2 Fix Base.decode16!/2 doc: base 16 has no padding (#15558) 2026-07-09 12:02:39 +02:00
nseaprotector 16bee46eef Fix Date.range/2 doc: step is range/3's third argument (#15557) 2026-07-09 11:40:32 +02:00
nseaprotector b639d87c9f Refine Enum.concat specs (#15554) 2026-07-09 11:16:53 +02:00
nseaprotector b3797d0cf5 Refine String.slice/3 return type to t (#15553)
The spec declared `grapheme` instead, whose typedoc is
"Multiple code points that may be perceived as a single
character", but it may return multiple graphemes.

`grapheme` is defined as `@type grapheme :: t`, so this is
identical to analysis tools.
2026-07-09 10:45:49 +02:00
Alexander Gubarev 065b8532c0 Tighten IO typespecs (#15555) 2026-07-09 10:44:24 +02:00
nseaprotector 0aa4acef63 Refine List.keyfind!/3 return type to tuple (#15556) 2026-07-09 10:43:44 +02:00
nseaprotector 233f824c16 Refine Enum.chunk_while/4 return type to [chunk] (#15552)
Unlike `chunk_by/2` and `chunk_every/2,4` (which always chunk into
sublists and are typed `[list]`), `chunk_while/4` lets the reducer emit an
arbitrary term as a chunk (`{:cont, chunk, acc}`, with `chunk: any`), so
`[chunk]` is the precise return type.
2026-07-09 08:57:06 +02:00
José Valim a3bfe9d407 Improve docs and code coments 2026-07-09 00:32:52 +02:00
nseaprotector 35cceec15b Improve MapSet.split_with/2 doctest and fix predicate arity (#15551) 2026-07-08 10:47:37 +02:00
nseaprotector 9c83ece467 Align Access.filter/1 spec with its truthy semantics (#15550) 2026-07-08 10:06:15 +02:00
nseaprotector 9e687da4a9 Fix IO.read/2 docs: integer count is in characters, not bytes (#15549)
`IO.read/2` is the Unicode-safe reader and delegates an integer count
to `:io.get_chars/3`, which counts characters, not bytes. The docs were
copied from `IO.binread/2` (the byte-oriented, Unicode-unsafe reader),
so they incorrectly described the integer argument as a number of bytes
and carried binread/2's "preferred mode for reading non-textual inputs"
note. This also contradicted read/2's own return description, which
already documents the result as "the output characters".

Assisted-by: Claude Code:claude-opus-4-8

Signed-off-by: nseaSeb <nseaprotector@gmail.com>
2026-07-08 09:52:41 +02:00
dependabot[bot] 681a9ee9cc Bump erlef/setup-beam from 1.24.0 to 1.24.1 (#15548)
Bumps [erlef/setup-beam](https://github.com/erlef/setup-beam) from 1.24.0 to 1.24.1.
- [Release notes](https://github.com/erlef/setup-beam/releases)
- [Commits](https://github.com/erlef/setup-beam/compare/fc68ffb90438ef2936bbb3251622353b3dcb2f93...54075bcc5e249e4758d363f27d099f55d843f124)

---
updated-dependencies:
- dependency-name: erlef/setup-beam
  dependency-version: 1.24.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-08 09:38:57 +02:00
José Valim 2fbe44ef64 Update forum notification endpoint 2026-07-07 14:22:42 +02:00
Alexander Gubarev ad993f0d80 Improve docstrings and typespecs (#15547) 2026-07-07 14:14:18 +02:00
José Valim 82fac2fd38 Batch cachers in parallel checker (#15545) 2026-07-06 17:19:01 +02:00
José Valim 2bcaaf8a62 Do not use term bodiless functions, closes #15537 2026-07-03 20:32:53 +02:00
Panagiotis Nezis 56e4e28d86 Fix spec of File.copy/3 (#15539) 2026-07-03 21:36:01 +09:00
Jean Klingler bec8d39796 Add :migrate_atom_interpolations option to formatter (#15531) 2026-07-02 08:04:25 +09:00
dependabot[bot] 6f9f2d1429 Bump actions/checkout from 6.0.3 to 7.0.0 (#15536)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.3 to 7.0.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/df4cb1c069e1874edd31b4311f1884172cec0e10...9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-01 13:07:18 +02:00
dependabot[bot] 1c8b15897b Bump zizmorcore/zizmor-action from 0.5.6 to 0.5.7 (#15535)
Bumps [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) from 0.5.6 to 0.5.7.
- [Release notes](https://github.com/zizmorcore/zizmor-action/releases)
- [Commits](https://github.com/zizmorcore/zizmor-action/compare/5f14fd08f7cf1cb1609c1e344975f152c7ee938d...192e21d79ab29983730a13d1382995c2307fbcaa)

---
updated-dependencies:
- dependency-name: zizmorcore/zizmor-action
  dependency-version: 0.5.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-01 13:07:04 +02:00
Neil Lyons e0d257251b Update Enum.reduce_while/3 type spec to use acc alias (#15534) 2026-07-01 09:40:06 +02:00
José Valim b3c0f259ef mix format 2026-07-01 09:39:40 +02:00
Aaron Tinio b680bea383 Document :opening_delimiter option for Mix.Tasks.Format format/2 (#15532)
Sigil callbacks have received :opening_delimiter in their opts since
v1.15.0, but it was never listed alongside :sigil and :modifiers
2026-06-28 16:49:51 +02:00
José Valim 9cb8bb63ce Check for missing requirement in the loader 2026-06-26 18:43:53 +02:00
José Valim 66ca149315 Clarify protocols come later, closes #15529 2026-06-26 16:55:25 +02:00
Łukasz Samson 68b660d8e8 Add missing Of.reset_vars call in receive/after badtimeout error path (#15527)
Variable refinement should not leak outside after block
2026-06-25 10:25:20 +02:00
Łukasz Samson a897d59df7 Refer to the right function in remote_apply error on :maps.values/1 (#15526) 2026-06-25 10:24:55 +02:00
Łukasz Samson bd36b69cec Enable doctests on Descr, fix stale examples (#15528) 2026-06-25 10:24:35 +02:00
José Valim eecafef2c0 Track dependency removal for local dependencies, closes #15445 2026-06-24 17:55:33 +02:00
José Valim 1ad6e735cb Track dependency removal for fetchable dependencies 2026-06-24 17:55:33 +02:00
Łukasz Samson 5fd13212d9 Thread full descr through fun_denormalize reduce (#15524)
Previously inner representation was threaded and returned, which broke expectations of caller `non_term_type_to_quoted`

Closes #15523.
2026-06-24 13:10:14 +02:00
José Valim 46c461e617 Expand defguard per guard and expression
Closes #15450.
Closes #15284.
2026-06-24 13:07:43 +02:00
Łukasz Samson 9a2029245d Improve error returned from fun_apply (#15522)
- return badfun on empty type
- exclude call arity on badarity tuple

Fixes #15521
2026-06-24 10:32:44 +02:00
Guillaume Duboc b4bd740eb7 Add put_dynamic for proper dynamic insertion (#15520) 2026-06-23 17:23:57 +02:00
José Valim 940d01c1c4 Fix arity in to_unsafe_atom 2026-06-23 11:00:19 +02:00
José Valim f4bf3fd20e Segregate bitstring and bitstring no binary in map domains 2026-06-23 10:43:35 +02:00
Łukasz Samson 1d3a5b85f4 Include bitstring in domain keys (#15516)
Fixes #15515
2026-06-23 09:46:40 +02:00
José Valim 34c7f2106c Add warnings around using Mix.Project in formatters 2026-06-22 10:02:23 +02:00
José Valim 42cf846de0 Better organize tests 2026-06-21 21:36:35 +02:00
Łukasz Samson 94dc133058 Correctly render list with negative leaves (#15514)
BDD leaves represent non empty lists

Fixes #15513.
2026-06-21 21:33:30 +02:00
Łukasz Samson 9347ccd564 Return error from compatible_intersection with none (#15512)
Match fast path behaviour with `compatible?/2`
Fixes #15511
2026-06-21 21:31:51 +02:00
Łukasz Samson 4215bbe986 Report empty_list: true on dynamic() in list_of (#15506)
Fixes #15505
2026-06-19 21:42:37 +02:00
José Valim f70e504ebb Fix optimized map difference with open key
Closes #15507.
Closes #15508.
2026-06-19 21:30:14 +02:00
José Valim 73cf46d785 Make sure to test both NFC and NFD formats 2026-06-18 11:29:05 +02:00
José Valim ec50a88ee4 Validate normalized tokenizer identifiers
Ensure identifiers that require normalization are checked again
after normalization so decomposed source cannot normalize into
unsupported codepoints.

Closes #15419.
2026-06-18 10:55:09 +02:00
José Valim 311f6fb57d Run formatter 2026-06-18 09:35:09 +02:00
Łukasz Samson 8e27ce49a3 Preserve optional static part on difference (#15503)
Fixes #15502
2026-06-18 09:21:36 +02:00
José Valim afedada0d1 Clarify state is the failed state 2026-06-18 09:15:38 +02:00
Barna Kovacs 1e12c7d6be Optimize Duration.add, subtract, multiply, negate (#15501) 2026-06-17 21:25:40 +02:00
José Valim 264200a4a6 Avoid unecessary umbrella recompilation when a path dependency's manifest is newer but unchanged, closes #15454 2026-06-17 15:54:55 +02:00
Barna Kovacs 39acd239af Optimize Keyword.validate/2 by adding fast-track list merging (#15499) 2026-06-17 10:53:51 +02:00
José Valim 1ac9007bae Fix race condition in test 2026-06-17 10:53:12 +02:00
dependabot[bot] 72e5d84b4e Bump github/codeql-action from 4.36.1 to 4.36.2 (#15500)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.36.1 to 4.36.2.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/87557b9c84dde89fdd9b10e88954ac2f4248e463...8aad20d150bbac5944a9f9d289da16a4b0d87c1e)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.36.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-17 09:13:51 +02:00
Łukasz Samson 0cdaea1a8c Fix definition of term_or_dynamic_optional type (#15498)
Fixes #15497
2026-06-17 08:47:31 +02:00
Jean Klingler a9976cc5d9 Rework anti-pattern doc about unsafe atom creation (#15495) 2026-06-17 15:39:24 +09:00
José Valim fce9b1c0d4 Update CHANGELOG 2026-06-17 08:37:57 +02:00
Jean Klingler 83e9b66e65 Add to_unsafe_atom/1 and soft-deprecate to_atom/1 (#15494) 2026-06-16 20:50:11 +09:00
Jean Klingler 8efdf4eb10 Add List.to_existing_atom/2 to validate against a list of allowed atoms (#15493) 2026-06-16 20:35:01 +09:00
Henrik Tudborg 56bc730a2a Avoid using Keyword.pop/2 in ExUnit.Diff.struct_module/2 (#15489)
`struct_module` is being called for both structs and maps.
`diff_quoted_struct` uses the return of `struct_module` to determine
when to call `diff_quoted_struct` or `diff_map`.
2026-06-16 11:10:58 +02:00
Jean Klingler fd1109e859 Add String.to_existing_atom/2 to validate against a list of allowed atoms (#15483) 2026-06-16 17:45:09 +09:00
José Valim 1d599978f7 Symlink AGENTS.md to CONTRIBUTING.md 2026-06-15 20:31:46 +02:00
José Valim b64b214412 Preserve empty list in dynamic list type constructor
Closes #15488.
2026-06-15 20:28:30 +02:00
José Valim c540d1fa94 Handle empty function type in fun_apply
Closes #15486.
2026-06-15 18:47:47 +02:00
Henrik Tudborg 75536d80f1 Add support for keyword lists in Access.key/2 and Access.key!/1 (#15470) 2026-06-15 16:04:10 +02:00
José Valim 0395301060 Preserve file meta from each clause in type system warnings 2026-06-15 16:03:47 +02:00
Zach Allaun 59434b1590 Document that mix test --dry-run was introduced in 1.20.0 (#15484) 2026-06-15 15:08:02 +02:00
José Valim 97770a660a Update CHANGELOG 2026-06-15 14:02:59 +02:00
José Valim 23d9bfd3e1 Do not leak vars between default args
Closes #15463.
2026-06-15 13:55:39 +02:00
José Valim 99cd654460 Handle rebound vars in bitstring size expansion
Closes #15479.
2026-06-15 13:45:55 +02:00
José Valim 45ef8fd94a Document @on_definition runs in reverse order, closes #15481 2026-06-15 12:49:08 +02:00
Łukasz Samson 784db65375 Fix missed optimization of Kernel.put_elem/3 (#15480)
?reorder added in 87582af54 used wrong arity and wrong erlang call
2026-06-15 12:16:54 +02:00
Jean Klingler 2421be6732 Fix bin comprehensions when option is used (#15473)
* Fix bin comprehensions - static size

* Fix bin comprehensions - pinned var size

* Fix bin comprehensions - operations

* Add tests for nested generators
2026-06-15 17:32:26 +09:00
Barna Kovacs d5e131623d Optimize behaviour_defined? (#15478) 2026-06-15 10:19:03 +02:00
Barna Kovacs 9046a2fe72 Optimize URI.to_string/1 (#15477) 2026-06-15 00:20:48 +02:00
Barna Kovacs 1796733076 Optimize unpercent with guard and by avoiding with clause (#15476) 2026-06-14 21:46:02 +02:00
Barna Kovacs c958e540e9 Optimize bag_distance by calling bag_difference only once 2026-06-14 20:31:27 +02:00
José Valim 36f272cdf0 Raise better error messages when expected structs disappear, closes #15472 2026-06-14 13:55:28 +02:00
Barna Kovacs efcdfb8eca Optimize MapSet.symmetric_difference/2 when sizes mismatched (#15471) 2026-06-14 12:58:17 +02:00
Jean Klingler df9d1d6e52 Cleaner error message for unquote when quote is used in a pattern (#15469) 2026-06-13 21:15:55 +09:00
dependabot[bot] 1094302c24 Bump github/codeql-action from 4.36.0 to 4.36.1 (#15448)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.36.0 to 4.36.1.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/7211b7c8077ea37d8641b6271f6a365a22a5fbfa...87557b9c84dde89fdd9b10e88954ac2f4248e463)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-13 14:13:29 +02:00
dependabot[bot] 03c42d723c Bump actions/checkout from 6.0.2 to 6.0.3 (#15449)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.2 to 6.0.3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/de0fac2e4500dabe0009e67214ff5f5447ce83dd...df4cb1c069e1874edd31b4311f1884172cec0e10)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-13 14:13:07 +02:00
José Valim 09e1448710 Simplify precision tracking in bitstrings 2026-06-13 12:11:14 +02:00
José Valim 502421c94e Fix env outdated deps compilation
We should only avoid cleaning for path dependencies.
Fetch dependencies should go through the previous process.

Closes #15467.
2026-06-13 12:08:52 +02:00
José Valim 47b762b4f1 Track reused variables in bitstring patterns 2026-06-13 11:17:26 +02:00
Barna Kovacs b48221d221 Optimize Keyword.pop/3, pop!/2, and pop_lazy/3 (#15466)
Eliminate redundant list traversal by calling :lists.keyfind + delete_key
directly instead of going through fetch/2 + delete/2.

The old code performed 3 traversals when the key was present:
1. :lists.keyfind (inside fetch)
2. :lists.keymember (inside delete — redundant)
3. delete_key (inside delete)

The new code performs 2 traversals:
1. :lists.keyfind (find the value)
2. delete_key (remove all matching entries)

Benchmark shows ~1.15x speedup on avg (noisy system)
2026-06-12 21:02:13 +02:00
José Valim 6f18bf70c0 Persist code paths and Code.compiler_options across partitions
Closes #15457
2026-06-12 21:00:46 +02:00
José Valim 82fa62d0a5 Use cyclic instead of recursive, add more examples, closes #15462 2026-06-12 18:50:54 +02:00
José Valim 83ac4fe29c Ignore additional callbacks in protocol implementation, closes #15456 2026-06-12 18:45:43 +02:00
José Valim 6a14c55681 Warn on unaligned binary segments
Closes #15465.
2026-06-12 18:06:05 +02:00
José Valim 3ba669da3e Log times to type check modules when profiling 2026-06-12 13:57:16 +02:00
Barna Kovacs 7ad36f1bcc Correct :as_list typo to :as_lists in inspect examples (#15464)
Fixes occurrences where :as_list was used instead of :as_lists
for the :charlists option in List documentation and the getting
started guide.
2026-06-12 13:15:13 +02:00
Jean Klingler 8032f1dd19 Add since info to recently added env vars (#15458) 2026-06-11 16:50:03 +09:00
José Valim 9f24a452f8 Provide bare and opt set operations (#15433) 2026-06-10 19:37:25 +02:00
Łukasz Samson fcf47f34ab Add missing required key to __info__(:struct) type (#15453) 2026-06-10 16:41:15 +02:00
José Valim 600b7c63d3 Support split EEx clause middle expressions
Allow clause-introducing middle expressions
such as else, rescue, and catch to be followed
by a whitespace-only template segment and
another middle expression. This supports EEx
like with/try else clauses written as separate
tags, for example <% else %> followed by <% _ -> %>.

Keep whitespace-only bodies for ordinary arrow
clauses, so adjacent empty case/cond clauses
continue to compile as before.
2026-06-10 15:15:41 +02:00
José Valim f8ffaf01b5 Document false positives, closes #15451 2026-06-10 09:27:32 +02:00
José Valim f594d98a15 Raise if a Git refspec starts with - as it is parsed as a CLI flag 2026-06-10 09:27:32 +02:00
Alexander Gubarev bb8480fc3c Typo fixes (#15447) 2026-06-10 00:25:22 +02:00
José Valim c64417d72f Limit version numbers to 14 bytes
This avoids parsing too large integers.

CVE-2026-49762
GHSA-w2h8-8x3g-278p
2026-06-09 15:25:57 +02:00
José Valim eacf0a9baf Add docs about URI encoding/to_string 2026-06-09 14:20:41 +02:00
José Valim 0cdfc80737 Validate paths and files when extracting archives 2026-06-09 14:15:27 +02:00
José Valim b2aad3d9f8 Clarify security considerations and disclaimers to archive/escript install 2026-06-09 13:37:58 +02:00
José Valim eab32c2797 Cap width in strftime to 1024 characters 2026-06-09 13:25:55 +02:00
Steffen Deusch 1cb62b731c Add trace helper to ExUnit.Assertions (#15432) 2026-06-08 14:17:55 +02:00
Aaron Tinio d714fb203b Honor --no-compile in mix format (#15443) 2026-06-07 21:34:23 +02:00
José Valim 65e6b08576 Tag lists member as returning boolean 2026-06-07 11:07:14 +02:00
Gary Rennie 9ee4bcf564 Add support for erlc_options and beam_debug_info chunk to support edb (#15438) 2026-06-07 10:53:57 +02:00
Łukasz Samson 8a8fbcf2b1 Add capture_arg to Code.Fragment.cursor_context spec (#15441) 2026-06-06 18:31:28 +02:00
Andrea LeopardiandJosé Valim 45fc548139 Add more docs to System.otp_release/0 (#15440)
Co-authored-by: José Valim <jose.valim@dashbit.co>
2026-06-06 13:45:16 +02:00
José Valim 9b65902446 Deal with empty canonical on doc generation 2026-06-05 15:08:18 +02:00
José Valim b897da7b4c require_file releases the file if compilation fails 2026-06-04 19:42:34 +02:00
José Valim d44ad8142a Do not discard metadata when proxying to elixir typespecs 2026-06-04 19:29:50 +02:00
dependabot[bot] 7f8a8c57a4 Bump github/codeql-action from 4.35.5 to 4.36.0 (#15436)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.35.5 to 4.36.0.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/9e0d7b8d25671d64c341c19c0152d693099fb5ba...7211b7c8077ea37d8641b6271f6a365a22a5fbfa)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.36.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 01:21:35 -07:00
aeturnumfluctus 7ff272706a Remove pluralization in RuntimeError documentation (#15435) 2026-06-03 16:17:25 +02:00
José Valim fa1ff52c39 Clarify registry docs, closes #15431 2026-06-02 10:28:15 +02:00
Raymond Loranger 55da97b9a3 Remove qualified call in IO.ANSI (#15430) 2026-06-01 21:49:25 +02:00
Jonatan Männchen b5f1864d4e Use package.hexdocs.pm URL format instead of hexdocs.pm/package (#15429) 2026-05-30 22:19:31 +02:00
dependabot[bot] 8ac03ec3b7 Bump zizmorcore/zizmor-action from 0.5.3 to 0.5.6 (#15423)
Bumps [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) from 0.5.3 to 0.5.6.
- [Release notes](https://github.com/zizmorcore/zizmor-action/releases)
- [Commits](https://github.com/zizmorcore/zizmor-action/compare/b1d7e1fb5de872772f31590499237e7cce841e8e...5f14fd08f7cf1cb1609c1e344975f152c7ee938d)

---
updated-dependencies:
- dependency-name: zizmorcore/zizmor-action
  dependency-version: 0.5.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-28 10:40:12 +02:00
dependabot[bot] 8f43d2bd2b Bump github/codeql-action from 4.35.4 to 4.35.5 (#15424)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.35.4 to 4.35.5.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/68bde559dea0fdcac2102bfdf6230c5f70eb485e...9e0d7b8d25671d64c341c19c0152d693099fb5ba)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.35.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-28 10:39:56 +02:00
dependabot[bot] 1f50ad0aa5 Bump azure/trusted-signing-action from 1.2.0 to 2.0.0 (#15422)
Bumps [azure/trusted-signing-action](https://github.com/azure/trusted-signing-action) from 1.2.0 to 2.0.0.
- [Release notes](https://github.com/azure/trusted-signing-action/releases)
- [Commits](https://github.com/azure/trusted-signing-action/compare/b443cf8ea4124818d2ea9f043cba29fc3ec47b16...c7ab2a863ab5f9a846ddb8265964877ef296ee82)

---
updated-dependencies:
- dependency-name: azure/trusted-signing-action
  dependency-version: 2.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-28 10:39:43 +02:00
José Valim f781a02a24 Unify reverse_arrow/guard_context handling 2026-05-27 11:10:16 +02:00
Guillaume Duboc 91e622889d Refine static predicate branches (#15426) 2026-05-27 10:48:55 +02:00
Jean Klingler db6e80277d Never suggest back invalid characters in tokenizer hint (#15421) 2026-05-27 10:23:12 +02:00
José Valim 23a431223b Optimize Registry exact key matching
Assisted-by: ChatGPT-5.5/Codex for test case generation.

Closes #15418.
2026-05-26 12:31:47 +02:00
dependabot[bot] 85ff5da403 Bump github/codeql-action from 4.35.3 to 4.35.4 (#15405)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.35.3 to 4.35.4.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/e46ed2cbd01164d986452f91f178727624ae40d7...68bde559dea0fdcac2102bfdf6230c5f70eb485e)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.35.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 12:15:54 +02:00
Jean Klingler 571c108503 Inline in empty list in erlang pass (#15413) 2026-05-24 09:05:42 +09:00
Łukasz Samson 92de556716 Fix spec of ex_unit capture_log options (#15417)
Passing `formatter: nil` was never supported and crashes.
2026-05-23 21:03:22 +02:00
Łukasz Samson 209b0b31ff Fix typo in JSON.Encoder moduledoc (#15416) 2026-05-23 19:33:39 +09:00
Łukasz Samson 274dbb6e10 Fix trapped signal documentation (#15415)
`sigstop` is untrappable on POSIX
`erl_signal_handler` implementation confirms that `sigterm` is trapped
https://github.com/erlang/otp/blob/bf6adb8744a589c89eb79c8ae9c49ca348f325fd/lib/kernel/src/erl_signal_handler.erl#L39
2026-05-23 10:05:26 +02:00
José Valim e17cb856a0 Avoid false positives on cascading warnings
Only emit warnings for missing exports in defined
modules to avoid false positives with modules from
the current project which may not have been defined
yet.
2026-05-22 10:41:13 +02:00
José Valim 32d5bf3f18 Improve tracking of g df within the type system 2026-05-22 10:14:30 +02:00
José Valim c494891344 Avoid warnings when generating docs 2026-05-22 10:05:14 +02:00
José Valim 5337ab8716 Revert expansion of or/and in guards
This partially reverts aad63173 as it caused
massive AST to generate when expanding guards
with `expr in [...]` checks in them, as each
`or` became a full-blown case expression.

Alternative approaches will be explored in
future releases.
2026-05-22 09:51:09 +02:00
José Valim 8336f33263 Wrap quote in a function that will implement recursive types
Closes #15410.
2026-05-22 09:51:09 +02:00
José Valim b08f6fa3e2 Avoid redundant emptiness checks on list/tuple emptiness (#15412) 2026-05-22 09:08:19 +02:00
Kevin Schweikert 97abb54442 Colorize CLI error banner (#15408)
The script-crash path in Kernel.CLI.print_error/3 now renders the
exception banner (e.g. "** (ArgumentError) ...") in red when
IO.ANSI.enabled?/0 is true. The stacktrace stays plain.

Assisted-by: claude-code:claude-opus-4-7
2026-05-21 15:05:13 +02:00
Michał Łępicki c4a63d3c11 Update historical version link heading from 1.19 to 1.20 in CHANGELOG.md (#15409) 2026-05-21 15:03:33 +02:00
José Valim 07e34342da Deprecate Macro.Env.fetch_alias/2 and Macro.Env.fetch_macro_alias/2 2026-05-21 13:52:13 +02:00
José Valim bb0362f3f6 Start v1.21 2026-05-21 13:37:31 +02:00
José Valim 1f19a053cf Improve coverage of large heads with more complex code 2026-05-21 13:32:19 +02:00
José Valim af6d6b2cbe Mark stale fetchable deps for compile before convergence
A fetched git dependency could report {:ok, vsn} from its
stale .app before Mix checked the stored SCM manifest lock.
If the same dependency then appeared again through a stricter
transitive requirement, the converger would mark it as
:divergedreq instead of :compile.

Validate the SCM manifest while loading the dependency so
fetchable deps with a stale stored lock are marked for
compilation before convergence reuses their status.
Add regression tests for the transitive git case and for
the direct converger case.

Closes #15406.
2026-05-21 12:54:55 +02:00
José Valim 4dd6c3cf6a Revert "Allow overriding specific dependencies in override (#15193)"
Hex uses the flag in a way that's not visibile to Mix,
so we have false positives. Closes #15407.

This reverts commit bc7af21c02.
2026-05-21 10:17:05 +02:00
José Valim 862ff780b8 Report clearer errors for violations on !, closes #15404 2026-05-20 16:09:24 +02:00
José Valim edc61ce9c0 Remove hardacoded line 2026-05-20 14:32:06 +02:00
José Valim d1e91dee7f Do not propagate variable refinement during inference 2026-05-20 13:39:33 +02:00
José Valim 7e7b4a8f4e Indent lists 2026-05-20 13:35:41 +02:00
José Valim f9573159ab Clarify cases optimization apply 2026-05-20 12:54:03 +02:00
José Valim 3d4bffe097 Remove duplicate nodes in BDD when performing unions
Our inference algorithm deduce types for each clause,
subtracting previous clauses. For a function with three
clauses, the type of each clause is roughtly:

    c1
    c2 - c1
    c3 - c2 - c1

To compute the domain, we union these clauses, leading
to large types that mix unions and differences. By removing
repeated nodes, the union of the entries above become:

    c1 or c2 or c3
2026-05-20 12:41:24 +02:00
José Valim 694930ec25 Add some initial resources to types page 2026-05-20 09:55:20 +02:00
José Valim 4725e1100c Update CHANGELOG 2026-05-19 18:59:36 +02:00
José Valim 76fbc0ccbc Do not discard variables in cond
When traversing the case for the reverse arrow, if there are
no changes, the case-expr are not re-executed, which meant
variables would not be redefined.

Closes #15402.
Closes #15403.
2026-05-19 13:15:58 +02:00
José Valim 28734f03d0 Clarify aliases is part of the next chapter, closes #15401 2026-05-18 20:50:15 +02:00
José Valim 0dd4399293 Simplify literal differences to avoid repacking 2026-05-17 15:57:31 +02:00
José Valim 0aa8c1fc2c Fix optimization in map leaf difference 2026-05-17 15:57:31 +02:00
Jean Klingler 315279701a Fix dialyzer opaqueness violation with MapSet (#15399) 2026-05-17 19:45:56 +09:00
José Valim 57b1685cac Collect diagnostics across clauses in rescue, closes #15307 2026-05-16 18:12:45 +02:00
José Valim 6962dc2d91 Deprecate xref: [exclude: ...] 2026-05-16 13:24:12 +02:00
Evadne Wu ca13b750ce Fix parse_error input spec (#15393) 2026-05-16 13:20:23 +02:00
José Valim c3e1aa706d Infer signatures across application boundaries, closes #15360 2026-05-16 13:14:06 +02:00
Evadne Wu b58e843528 Scope ExUnit diff width tests (#15396) 2026-05-16 01:27:33 +02:00
Łukasz Samson 0418986004 Fix Mix.Task.requirements/1 spec (#15394) 2026-05-15 21:45:22 +02:00
Evadne Wu 210f6fc3ac Clarify File.cp_r directory permission docs (#15392) 2026-05-15 19:27:24 +02:00
Jonatan Kłosko 536e53b173 Respect --raise when mix test --warnings-as-errors passes with warnings (#15390) 2026-05-15 10:30:28 +02:00
José Valim 64d381bd05 Avoid warnings when editor is not set 2026-05-14 21:55:35 +02:00
José Valim 87582af546 Preserve evaluation order when rewriting (#15389)
This was done by moving the rewriting which change evaluation order to the Erlang pass,
otherwise would be hard to rewrite back in the type checker reports.

Closes #15310.
2026-05-14 21:16:55 +02:00
Guillaume Duboc 63708d313c Type system support for put_elem/3 (#15388) 2026-05-14 20:45:33 +02:00
Guillaume Duboc e7a5a667bf Make tuple_insert_at preserve static part of a type (#15387) 2026-05-14 20:44:54 +02:00
Guillaume Duboc 367e38cbe6 Fix if_set dropping static part of type (#15379) 2026-05-14 20:44:22 +02:00
José Valim cb4020e692 Simplify handling of negative indexes 2026-05-14 18:25:58 +02:00
Łukasz Samson 909b099d44 Fix dynamic-value error wrapping in tuple_insert_at/3 (#15382)
Propagate `badtuple` and `badindex` error from `tuple_insert_at_checked` to the caller instead of wrapping it in `dynamic`
2026-05-14 16:23:46 +02:00
José Valim 7495a03535 Revert "Improve Keyword performance (#15378)" (#15386)
This reverts commit d50b0321ab.
2026-05-14 16:18:50 +02:00
José Valim 5509b95437 Group clauses by return, closes #15371 2026-05-14 15:38:47 +02:00
Łukasz Samson 0792999f63 Fix invalid error message on tuple operations with negative index (#15380) 2026-05-14 15:38:28 +02:00
Peter Ullrich d50b0321ab Improve Keyword performance (#15378) 2026-05-14 15:23:00 +02:00
José Valim 41332ac3d6 Silence warnings on charlist formatting 2026-05-14 14:03:32 +02:00
José Valim 46f7b37916 Ignore eval failure warnings from Erlang
Closes #15372.
2026-05-14 13:59:28 +02:00
José Valim 6b114ee7c1 Avoid false positives when comparing structs with dynamic
Closes #15370
2026-05-14 13:53:25 +02:00
Guillaume Duboc 71148aa953 Tuple operations performance boost + fix logic bug (#15376) 2026-05-14 12:44:18 +02:00
José Valim b35e210321 Properly format or/and in type errors
See #15369.
2026-05-14 12:43:28 +02:00
Cohen Carlisle 56336feb2f Mirror Map.filter/2 perf note in Map.reject/2 (#15373) 2026-05-13 23:23:03 +02:00
José Valim bf47be54bc Add a note about searching existing isues 2026-05-13 18:57:19 +02:00
José Valim aad63173b8 Clarify semantics of defguard, closes #15284 2026-05-13 18:53:13 +02:00
José Valim 6050b8400a Release v1.20.0-rc.5 2026-05-13 16:15:18 +02:00
José Valim 42a005e4df Bump CI to OTP 29 (#15367) 2026-05-13 14:52:22 +02:00
José Valim c2d39715ff Update CHANGELOG 2026-05-13 11:45:50 +02:00
Guillaume Duboc 472a49d84a Preserve static parts in dynamic container descriptors (#15363) 2026-05-13 11:45:23 +02:00
dependabot[bot] a631da4fb0 Bump github/codeql-action from 4.35.2 to 4.35.3 (#15365)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.35.2 to 4.35.3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/95e58e9a2cdfd71adc6e0353d5c52f41a045d225...e46ed2cbd01164d986452f91f178727624ae40d7)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.35.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-13 10:37:01 +02:00
dependabot[bot] 7f0670e574 Bump DavidAnson/markdownlint-cli2-action from 23.1.0 to 23.2.0 (#15364)
Bumps [DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action) from 23.1.0 to 23.2.0.
- [Release notes](https://github.com/davidanson/markdownlint-cli2-action/releases)
- [Commits](https://github.com/davidanson/markdownlint-cli2-action/compare/6b51ade7a9e4a75a7ad929842dd298a3804ebe8b...ded1f9488f68a970bc66ea5619e13e9b52e601cd)

---
updated-dependencies:
- dependency-name: DavidAnson/markdownlint-cli2-action
  dependency-version: 23.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-13 10:36:13 +02:00
José Valim 8bdd041122 Add missing exclamation mark 2026-05-13 10:35:57 +02:00
José Valim 8f264f3ced Include you must disclose the use of AI 2026-05-13 10:31:32 +02:00
José Valim 0eabf48524 Refine types from case on inference 2026-05-12 20:15:16 +02:00
Guillaume Duboc b0db1a26e1 Performance boost on map operations + bugfix (#15362)
Add the two important optimization paths for negative-map projection: value-side projection for map fetch/get and shape-side projection for map put/update.

Keep regression coverage for projected negative maps.
2026-05-12 18:50:17 +02:00
José Valim 35eeab3511 Fix regression on default inference, closes #15359 2026-05-12 14:59:53 +02:00
José Valim aae39c87dc Fix warnings 2026-05-12 14:49:36 +02:00
José Valim 8a2c815c85 Propagate hd/tl checks across clauses, closes #15358 2026-05-12 14:06:08 +02:00
José Valim 0b9b5b962b Improve type system docs 2026-05-12 10:58:11 +02:00
José Valim 41353c6cf8 Infer precise types for default arguments 2026-05-12 10:40:25 +02:00
José Valim 7e49585d26 Use inlined functions to reduce compiled module size 2026-05-11 23:36:21 +02:00
Peter Ullrich 7081e7223e Add SWAR versions of Base validations (#15357) 2026-05-11 23:29:52 +02:00
José Valim 3e3ce13c1d Deal with raising clauses in cond 2026-05-11 12:45:42 +02:00
José Valim 4816d3773a Perform occurrence typing in cond 2026-05-11 11:51:03 +02:00
José Valim 5b8b8e3589 Reintroduce escaped trailing newlines in heredocs in Macro.to_string/2 (#15355)
Closes #15354.
2026-05-11 10:18:49 +02:00
Cohen Carlisle aafc248179 fix ===/2 arity in docs (#15353) 2026-05-10 09:34:03 +09:00
Łukasz Samson b8723fea1e Add missing spec for documented deps property in Mix.Dep (#15351) 2026-05-08 22:42:20 +02:00
José Valim 9412f483d7 Simplify logger test 2026-05-08 22:31:09 +02:00
Łukasz Samson 8a35ffa811 Reject negative Duration in to_timeout (#15352)
Previously a negative timeout value violating the spec was returned
2026-05-08 22:28:48 +02:00
Łukasz Samson bb21320928 Persist log level to app env in Logger.configure/1 (#15348)
Fixes #15347
2026-05-08 11:02:04 +02:00
Łukasz Samson a5435a6ead Reject invalid config shapes in Config.validate!/2 (#15349)
Previous code used `Enum.all?` and silently skipped invalid pairs
2026-05-08 11:01:32 +02:00
José Valim 35497755b5 Use non_executable_binary_to_term on loopback pubsub 2026-05-06 23:42:55 +02:00
Łukasz Samson c3f7e3473e Raise in Stream.cycle when enumerable reduce call yields no elements (#15344) 2026-05-06 12:22:53 +02:00
Łukasz Samson 68c413f76c Improve ambiguous import error message (#15335) 2026-05-06 12:22:34 +02:00
Łukasz Samson 16f5174580 Add missing String.count/2 clause for empty pattern list (#15342) 2026-05-06 09:26:19 +02:00
José Valim 1254dae84f Avoid checking date validity twice 2026-05-06 08:28:52 +02:00
dependabot[bot] 651bdb485d Bump DavidAnson/markdownlint-cli2-action from 23.0.0 to 23.1.0 (#15345)
Bumps [DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action) from 23.0.0 to 23.1.0.
- [Release notes](https://github.com/davidanson/markdownlint-cli2-action/releases)
- [Commits](https://github.com/davidanson/markdownlint-cli2-action/compare/ce4853d43830c74c1753b39f3cf40f71c2031eb9...6b51ade7a9e4a75a7ad929842dd298a3804ebe8b)

---
updated-dependencies:
- dependency-name: DavidAnson/markdownlint-cli2-action
  dependency-version: 23.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-06 08:07:22 +02:00
José Valim 46ebf7cc83 Include additional AI usage guidelines 2026-05-04 19:35:58 +02:00
dependabot[bot] abf9ba1ae2 Bump github/codeql-action from 4.35.1 to 4.35.2 (#15321)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.35.1 to 4.35.2.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/c10b8064de6f491fea524254123dbe5e09572f13...95e58e9a2cdfd71adc6e0353d5c52f41a045d225)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.35.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-04 18:56:28 +02:00
José Valim 46811eccc9 Optimize difference of two equal bdds 2026-05-04 14:15:40 +02:00
José Valim 9199e5595c Ensure consistent ordering for closed elements 2026-05-04 13:01:51 +02:00
José Valim b8a29e0c1a Use :erlang.phash2 with tuples as it is much faster 2026-05-04 12:32:24 +02:00
José Valim ab5b33d8ac Avoid allocating tuples on bdd_map/bdd_reduce 2026-05-04 12:19:31 +02:00
Guillaume Duboc c376c0feb6 Hash-cons BDD descriptors (#15332) 2026-05-04 11:55:53 +02:00
Guillaume Duboc 5858e7c470 Fix wrong intersection with union of dynamic() (#15340) 2026-05-04 10:20:51 +02:00
Guillaume Duboc 2fd350672b Fix truthiness implementation (#15339) 2026-05-04 10:18:58 +02:00
José Valim 002e382794 Raise when loading struct info 2026-05-04 10:09:24 +02:00
José Valim 46841a22e4 Test we consider external variables as not precise, fix CI 2026-05-04 09:52:24 +02:00
José Valim bdf8e4481d Refine reverse arrows when only one clause remains 2026-05-04 01:24:42 +02:00
José Valim 3120d4539b Type checked struct operations must not deadlock the compiler 2026-05-04 01:24:42 +02:00
José Valim facde14525 Inline struct loading 2026-05-04 01:24:42 +02:00
José Valim 40e930607a Use definition instead of origin in meta 2026-05-04 01:24:42 +02:00
Peter Ullrich 03c123a149 Stop iterating over list when slice range ends (#15336) 2026-05-02 23:36:18 +02:00
Guillaume Duboc 81975a2de1 Fix descr edge cases (#15338) 2026-05-02 23:15:51 +02:00
Peter Ullrich 133dfa8d46 Provide multiple validate clauses for base16 (#15337)
It is consistent with the other implementations.
2026-05-02 22:56:52 +02:00
Łukasz Samson 39d646f459 Include sigils option in invalid import error message (#15334) 2026-05-01 23:04:20 +02:00
Łukasz Samson ad262dc8e4 Fix Kernel.ParallelCompiler.compile_opts each_cycle spec (#15333)
The callback invocation and default are both 0-arity.
2026-05-01 22:51:08 +02:00
Peter Ullrich a0e1604fe6 In EEX Compiler, flatten expr list only once, not on every iteration (#15331) 2026-05-01 18:13:52 +02:00
Peter Ullrich 11f7d8f84a Refactor Float.round/2 algorithm for better performance (#15329)
The previous implementation noted the trade-off:

```elixir
# 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.
```

It computed `m * power_of_5(count)` where `count` could grow to ~104, producing 250-bit bignums for a single division. The cost grew with both float magnitude and target precision.

### Benchmark results

11 different float workloads, measured with Benchee on Apple M2. Reported times are the median μs of one iteration over all 11 workloads.

precision | current | cox (this PR) | native_double | cox vs current | native_double vs current | native_double vs cox |
|---|---|---|---|---|---|---|
| 0 | 0.25 μs | 0.25 μs | 0.25 μs | 1.1× | 1.1× | ~tied |
| 1 | 2.42 μs | 0.33 μs | 0.33 μs | **7.1×** | 7.0× | ~tied |
| 2 | 2.67 μs | 0.63 μs | 0.33 μs | **3.8×** | 7.7× | 2.0× |
| 3 | 4.29 μs | 0.63 μs | 0.33 μs | **5.8×** | 11.9× | 2.1× |
| 5 | 5.50 μs | 0.67 μs | 0.33 μs | **6.8×** | 15.6× | 2.3× |
| 8 | 7.21 μs | 0.96 μs | 0.38 μs | **7.5×** | 19.5× | 2.6× |
| 12 | 7.75 μs | 0.96 μs | 0.38 μs | **8.2×** | 20.3× | 2.5× |
| 15 | 8.58 μs | 1.58 μs | 0.38 μs | **5.3×** | 22.8× | 4.3× |

Median μs per iteration over the 11-float mixed workload (per-call median ≈ value / 11).

- **`current`** is the bignum-based implementation we're replacing
- **`cox`** is this PR
- **`native_double`** is included as a "what does this look like in pure native float arithmetic" reference (`:erlang.round(f * pow) / pow`) - it's 2-4× faster than Cox but incorrect on tie inputs because `5.5675 * 1000` rounds to `5567.5` in IEEE due to double-rounding (multiplication round + integer round), then bumps to `5568`. It's why we can't simply use the obvious approach. Cox preserves exact semantics by working on the float's true binary representation.

#### Floats benchmarked

```
floats = [
  {"small_pi",       3.141592653589793},   # typical small value
  {"common_money",   5.5675},               # tie-boundary case
  {"large_e10",      1.2345e10},            # large magnitude
  {"tiny_e_neg8",    1.2345e-8},            # very small magnitude
  {"near_zero",      0.00001},              # close to zero
  {"negative",       -123.456789},          # negative sign
  {"integer_valued", 12.0},                 # exactly representable integer
  {"tie_half",       12.5},                 # half-tie
  {"tie_5675",       5.5675},               # documented tie example
  {"large_e30",      1.234e30},             # very large
  {"max_finite_ish", 1.7e308}               # near max double
]
```
2026-05-01 12:59:07 +02:00
Łukasz Samson f4e1b34617 Fix incorrect guard type refinement with length/map_size and != operator (#15327)
Previously `empty_list()` and `empty_map` were returned for `length(x) != n` and `map_size(x) != n` when n > 0. Now the types are widened to `list()` and `open_map()`
2026-04-30 18:30:07 +02:00
José Valim 55a3899e75 Add more tests around precision 2026-04-29 22:10:46 +02:00
Łukasz Samson ada2817d72 Fix invalid type inference on size compare in guards (#15324) 2026-04-29 22:06:23 +02:00
Guillaume Duboc 89e0106afc Fix map union optimization for open maps (#15322) 2026-04-29 08:48:16 +02:00
Łukasz Samson e39a1ca796 Consistently return path as binary in relative_to_cwd (#15316)
Previously if path parameter was passed in as chardata it was returned as is on `:file.get_cwd` failure
All `relative_to` and `join` code paths normalize to binary
2026-04-28 22:51:09 +02:00
Łukasz Samson 007efde2d5 Fix reentrancy of Code.eval_* (#15306)
Keep env and dbg callbacks on stacks in process dict

Closes #15303
2026-04-28 19:38:28 +02:00
Guillaume Duboc eb8bbc9c29 Fix map difference union optimization (#15319) 2026-04-28 19:12:34 +02:00
José Valim c5332235ab Perform all side-effect in order in in/2 2026-04-27 12:25:49 +02:00
José Valim e5d8b00e5b Remove maps:put/3 inlining, as it is done by the Erlang compiler, closes #15313 2026-04-27 12:03:24 +02:00
José Valim 2ba06cc40b Make preserving dir permissions opt-in, closes #15308 (#15312) 2026-04-26 19:08:23 +02:00
José Valim 68aae43703 Revert "Add each_directory to File.rm_rf, closes #15308 (#15311)"
This reverts commit d02e61d75e.

It did not address the original root problem.
2026-04-26 18:02:46 +02:00
Rafał Studnicki 2019de2d82 Switch {:duplicate, :key} key_ets to ordered_set with composite keys (#15304)
Switch {:duplicate, :key} key_ets to ordered_set

Change init_key_ets to create an ordered_set table for {:duplicate, :key}
and add ordered boolean to Partition state.
2026-04-26 11:12:04 +02:00
José Valim d02e61d75e Add each_directory to File.rm_rf, closes #15308 (#15311) 2026-04-26 10:39:46 +02:00
dependabot[bot] f2f2da9b40 Bump zizmorcore/zizmor-action from 0.5.2 to 0.5.3 (#15300)
Bumps [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) from 0.5.2 to 0.5.3.
- [Release notes](https://github.com/zizmorcore/zizmor-action/releases)
- [Commits](https://github.com/zizmorcore/zizmor-action/compare/71321a20a9ded102f6e9ce5718a2fcec2c4f70d8...b1d7e1fb5de872772f31590499237e7cce841e8e)

---
updated-dependencies:
- dependency-name: zizmorcore/zizmor-action
  dependency-version: 0.5.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-26 09:37:48 +02:00
dependabot[bot] 13d412864c Bump actions/upload-artifact from 7.0.0 to 7.0.1 (#15299)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 7.0.0 to 7.0.1.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/bbbca2ddaa5d8feaa63e36b76fdaad77386f024f...043fb46d1a93c77aae656e7c1c64a875d1fc6a0a)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-26 09:34:15 +02:00
Łukasz Samson 76d3fbad88 Disallow raw CR line ending in strings, comments and after ? (#15307) 2026-04-26 09:32:12 +02:00
José Valim b8a9dc60a5 Explicitly raise instead of calling assert(false) 2026-04-25 17:47:25 +02:00
José Valim 870d28fe64 Fix regression where we inferred the wrong type in a case branch that raises 2026-04-25 16:55:40 +02:00
José Valim 2d6e61c4f5 Compute negated types from precise none() branches 2026-04-22 22:11:26 +02:00
Łukasz Samson 81dc36568b Enrich EEx.SyntaxError with error metadata (#15297)
Fixes #15296
2026-04-22 16:58:24 +02:00
Łukasz Samson 99e0fe50a3 Fix Macro.Env.define_require/4 spec (#15302)
It may error when called with invalid `as` option
2026-04-22 15:34:15 +02:00
Łukasz Samson 8a4915008d Fix Macro.expand_literals/3 spec (#15301) 2026-04-22 15:32:40 +02:00
José Valim 0fe5b13320 Perform reverse arrows on with even when not precise 2026-04-22 12:18:03 +02:00
José Valim 808075d87a Implement reverse arrows for with 2026-04-21 20:17:08 +02:00
Łukasz Samson c14f3c7acb Fix arity of Logger.configure in warning message (#15298) 2026-04-21 22:04:04 +09:00
Łukasz Samson 5e7190c39f Fix static_atoms_encoder docs and spec (#15294)
Fixes #15293
2026-04-21 13:42:33 +02:00
Łukasz Samson 4ad7497d0e Consistently validate column option passed to parser (#15295)
Apply the same guard `line` option uses
2026-04-21 13:41:04 +02:00
Dinesh Anbazhagan 25cbbfe5d2 Fix errors in getting-started and mix-and-otp guides (#15289) 2026-04-20 20:34:28 +02:00
José Valim a5e19ad385 Support --output in mix deps.tree and mix app.tree, closes #15288 2026-04-20 08:17:12 +02:00
José Valim 983e033863 Do not touch sources when external resources change
Instead we store the changed files in the checkpoint.

This also address an issue where Elixir would say
"mtime was set in the future" when compiled too quickly.

See 7d521de. Closes #15277.
2026-04-18 22:26:29 +02:00
José Valim 890d9bbf00 Type checking improvements
* Ensure we do not accumulate previous information across clauses
* Avoid union(a, not a) when building domains
* Ensure union with an open map/tuples returns the open map/tuple
* Remove deep traversal on bdd leaf intersection which was slowing
  compilation of projects like open_api_spex

Closes #15285.
2026-04-18 18:13:48 +02:00
José Valim 20fef8cd1c Change directory permissions after writing, closes #15286 2026-04-18 09:11:57 +02:00
Guillaume Duboc 9231515db8 Bdd negation and difference optimizations (#15287) 2026-04-17 17:24:44 +02:00
José Valim d5e032efd1 Increase inspect limit to help print deeply nested data structures 2026-04-17 08:50:19 +02:00
Łukasz Samson 5e44c78fff Fix compiler crash on invalid nested bitstring (#15280) 2026-04-16 15:32:48 +02:00
Lukasz Samson 4d56bdd898 Do not reset quote level on calls in has_unquotes/2
The bug was introduced in 146fb4e3ff

Signed-off-by: José Valim <jose.valim@gmail.com>
2026-04-16 13:49:37 +02:00
José Valim 2e9981d98c Raise when splicing cons without head, closes #15282 2026-04-16 13:48:08 +02:00
Wojtek Mach ed478e816b Don't crash when inspecting OTP 29 native records (#15278) 2026-04-16 13:32:57 +02:00
José Valim 370e7db14a Ensure compiler modules are returned on failures, closes #15279 2026-04-16 08:26:16 +02:00
José Valim d67cb9634b Add performance regression test for open and closed maps 2026-04-15 21:07:24 +02:00
José Valim a37db60f78 Move :open check inside leaf_bdd_intersection 2026-04-15 19:38:04 +02:00
José Valim 116f8f472f Also optimize bottoms in leaf bdd intersections 2026-04-15 17:11:56 +02:00
José Valim 9c1bd76b26 Remove expected intersection as now track underscores 2026-04-15 17:11:56 +02:00
José Valim 3cb0b51836 Add AI contributions guidelines to CONTRIBUTING.md
Inspired by the recent Linux guidelines.
2026-04-15 14:08:30 +02:00
Gary Rennie 5b97e9c132 Show remaining runs when using --repeat-until-failure (#15270)
The --repeat-until-failure can be very useful for finding flaky tests,
however sometimes it is hard to gauge how long it has left to run before
it can be considered successful.

This commit adds a small output showing the number of remaining runs
when the `--repeat-until-failure` flag is used.

It looks like:

```
Running ExUnit with seed: 382462, max_cases: 32, remaining_runs: 3
Excluding tags: [windows: true]

.......
Finished in 0.02 seconds (0.00s async, 0.02s sync)

Result: 7 passed
Running ExUnit with seed: 422319, max_cases: 32, remaining_runs: 2
Excluding tags: [windows: true]

.......
Finished in 0.00 seconds (0.00s async, 0.00s sync)
```
2026-04-15 09:46:55 +02:00
José Valim 8ba8ad709f Document halt responsibilities for resources
Closes #15265.
Closes #15266.
2026-04-15 09:09:58 +02:00
José Valim 37dc125f2a Check both expand and dispatch are not in stacktrace 2026-04-15 09:01:42 +02:00
José Valim 9fb8fb1216 Add comments justifying use of subtyping, closes #15273 2026-04-15 08:59:22 +02:00
Łukasz Samson 5a1fa351dc Fix invalid module reference in prune_stacktrace (#15272)
Closes #15271
2026-04-15 08:59:08 +02:00
Jean Klingler 20cd669a0d Optimize Keyword.validate/2 to avoid building new list (#15269) 2026-04-15 07:06:59 +09:00
José ValimandGuillaume Duboc 03b45e521d Remove redundant line empty when we eliminate negations (#15196)
Co-authored-by: Guillaume Duboc <guilduboc@gmail.com>
2026-04-13 21:41:32 +02:00
José Valim bcfefa18ed Simplify Enum.dedup for lists 2026-04-13 21:37:57 +02:00
Łukasz Samson b7a832ea32 Add missing catch-all in :elixir_import.is_sigil/1 (#15264)
Fixes a compiler crash when importing a module with
only: :sigils option when the imported module exports
non sigil symbols with `sigil_` prefix.

Closes #15263.
2026-04-13 20:25:33 +02:00
José Valim 8011552eac Remove ?break handling duplication 2026-04-13 19:42:28 +02:00
José Valim ebb7c73353 Improve inference across for and try 2026-04-13 19:31:08 +02:00
Łukasz Samson 898f1a2f69 Reject unsupported newlines in tokenizer ASCII fast path (#15262) 2026-04-13 19:30:31 +02:00
José Valim 603602e67b Implement reverse arrows for case (#15260) 2026-04-13 15:21:08 +02:00
Emiliano Mancuso 92221937d3 Return {0, -1} for missing files in last_modified_and_size/1, closes #15258 (#15259) 2026-04-13 08:25:49 +02:00
José Valim 245066b405 Relax handling of __exception__
We were used to assert it to be true but,
given the value is not used in any shape,
we can relax the definition and simply assert
on the presence of the key.

Closes #15251.
2026-04-11 17:42:01 +02:00
José Valim 4d30464ec4 Avoid conflicts between internal vars and empty vars, closes #15246 2026-04-11 17:25:12 +02:00
José Valim 139da8fa5f Only check for lock files in non-path dependencies (see #15158) 2026-04-11 16:55:54 +02:00
José Valim 8fdef5c48f Properly track variables inside comprehensions size, closes #15247 2026-04-11 12:24:44 +02:00
José Valim d0514dfd15 Clean up typing changes 2026-04-11 12:17:16 +02:00
Guillaume Duboc 325608b518 Avoid applying map update callbacks to absent branches (#15248) 2026-04-11 12:06:29 +02:00
Guillaume Duboc c4275e39b2 Reject empty args in dynamic fun_apply (#15254) 2026-04-11 12:00:15 +02:00
Andrea Leopardi 1bf7bb0682 Fix param name in docs for NaiveDateTime functions (#15249) 2026-04-11 10:20:55 +02:00
dependabot[bot] 7d5d1e27ca Bump erlef/setup-beam from 1.23.0 to 1.24.0 (#15242)
Bumps [erlef/setup-beam](https://github.com/erlef/setup-beam) from 1.23.0 to 1.24.0.
- [Release notes](https://github.com/erlef/setup-beam/releases)
- [Commits](https://github.com/erlef/setup-beam/compare/ee09b1e59bb240681c382eb1f0abc6a04af72764...fc68ffb90438ef2936bbb3251622353b3dcb2f93)

---
updated-dependencies:
- dependency-name: erlef/setup-beam
  dependency-version: 1.24.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-11 10:20:37 +02:00
dependabot[bot] 50cc7ef182 Bump github/codeql-action from 4.32.4 to 4.35.1 (#15241)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.32.4 to 4.35.1.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/89a39a4e59826350b863aa6b6252a07ad50cf83e...c10b8064de6f491fea524254123dbe5e09572f13)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.35.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-11 10:19:42 +02:00
dependabot[bot] 5ec106a5e2 Bump DavidAnson/markdownlint-cli2-action from 22.0.0 to 23.0.0 (#15240)
Bumps [DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action) from 22.0.0 to 23.0.0.
- [Release notes](https://github.com/davidanson/markdownlint-cli2-action/releases)
- [Commits](https://github.com/davidanson/markdownlint-cli2-action/compare/07035fd053f7be764496c0f8d8f9f41f98305101...ce4853d43830c74c1753b39f3cf40f71c2031eb9)

---
updated-dependencies:
- dependency-name: DavidAnson/markdownlint-cli2-action
  dependency-version: 23.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-11 10:19:36 +02:00
dependabot[bot] 5c845c796c Bump azure/trusted-signing-action from 1.1.0 to 1.2.0 (#15225)
Bumps [azure/trusted-signing-action](https://github.com/azure/trusted-signing-action) from 1.1.0 to 1.2.0.
- [Release notes](https://github.com/azure/trusted-signing-action/releases)
- [Commits](https://github.com/azure/trusted-signing-action/compare/87c2e83e6868da99d3380aa309851b32ed9a8346...b443cf8ea4124818d2ea9f043cba29fc3ec47b16)

---
updated-dependencies:
- dependency-name: azure/trusted-signing-action
  dependency-version: 1.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-11 10:19:21 +02:00
Guillaume Duboc 469acb110f Fix guard equality with short-circuit operators
Signed-off-by: José Valim <jose.valim@gmail.com>

Closes #15253.
Closes #15257.
2026-04-11 10:16:59 +02:00
José Valim 7061c37b88 Add --no-compile to mix format
Closes #15256.
2026-04-10 12:19:25 +02:00
Nelson Vides 74bb90a3cd SWAR-optimize ASCII fast paths in String.length and String.slice (#15255)
Add 56-bit SWAR (SIMD Within A Register) acceleration to skip_length/2
and byte_size_remaining_at/2, processing 8 ASCII bytes per iteration
instead of one. Uses the Mycroft zero-byte detection algorithm to
validate that 7+1 bytes are all ASCII with no \r in a single guard.

This mirrors the approach taken in OTP's string module
(OTP https://github.com/erlang/otp/pull/10948).
2026-04-09 23:22:54 +02:00
Lukasz Samson 5d794ab17c Pass correct bitstring meta during expansion
Closes #15244
2026-04-08 18:42:12 +02:00
José Valim c1256f161c Clarify shut down order in ExUnit, closes #15236 2026-04-05 09:46:06 +02:00
José Valim 8f40bf117a Add "mix source MODULE" to print or open a given module/function location (#15234) 2026-04-04 21:11:25 +02:00
José Valim 8d691697ff Support types and callbacks in mix help 2026-04-04 20:01:18 +02:00
José Valim 6f2e24ca63 Add source/1 to IEx 2026-04-04 19:55:26 +02:00
José Valim 8cc693864a Improve docs for start_link on supervisor and genserver, closes #15232 2026-04-04 11:17:19 +02:00
José Valim 0f8baa3041 Remove test with race condition (already covered in warnings_test.exs) 2026-04-04 11:17:19 +02:00
Jonatan Kłosko 017162a975 Do not call fsync in lock implementation (#15229) 2026-04-02 15:40:45 +02:00
Jean Klingler f74b3648e2 Correct mention to after_compile/2 in changelog (#15224) 2026-03-31 20:56:29 +09:00
dependabot[bot] 28b6616a59 Bump actions/download-artifact from 8.0.0 to 8.0.1 (#15214)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 8.0.0 to 8.0.1.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3...3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: 8.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 10:22:44 +02:00
dependabot[bot] 7c2d4f3d8e Bump actions/attest-sbom from 4.0.0 to 4.1.0 (#15213)
Bumps [actions/attest-sbom](https://github.com/actions/attest-sbom) from 4.0.0 to 4.1.0.
- [Release notes](https://github.com/actions/attest-sbom/releases)
- [Changelog](https://github.com/actions/attest-sbom/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-sbom/compare/07e74fc4e78d1aad915e867f9a094073a9f71527...c604332985a26aa8cf1bdc465b92731239ec6b9e)

---
updated-dependencies:
- dependency-name: actions/attest-sbom
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 10:22:28 +02:00
dependabot[bot] ba4eb73734 Bump azure/login from 2.3.0 to 3.0.0 (#15212)
Bumps [azure/login](https://github.com/azure/login) from 2.3.0 to 3.0.0.
- [Release notes](https://github.com/azure/login/releases)
- [Commits](https://github.com/azure/login/compare/a457da9ea143d694b1b9c7c869ebb04ebe844ef5...532459ea530d8321f2fb9bb10d1e0bcf23869a43)

---
updated-dependencies:
- dependency-name: azure/login
  dependency-version: 3.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 10:22:03 +02:00
dependabot[bot] fbece98a5d Bump erlef/setup-beam from 1.20.4 to 1.23.0 (#15211)
Bumps [erlef/setup-beam](https://github.com/erlef/setup-beam) from 1.20.4 to 1.23.0.
- [Release notes](https://github.com/erlef/setup-beam/releases)
- [Commits](https://github.com/erlef/setup-beam/compare/e6d7c94229049569db56a7ad5a540c051a010af9...ee09b1e59bb240681c382eb1f0abc6a04af72764)

---
updated-dependencies:
- dependency-name: erlef/setup-beam
  dependency-version: 1.23.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 10:21:42 +02:00
José Valim 9e4eb3dc49 Release v1.20.0-rc.4 2026-03-31 09:55:32 +02:00
José Valim 2916f20189 Keep warnings on map/remote dispatch with/without parens
Closes #15219.
2026-03-30 10:27:58 +02:00
José Valim 03b9fde67a Allow test names longer than 255 characters 2026-03-29 09:13:46 +02:00
cui f16d2a32c1 Remove range overlap in Logger truncation (#15221) 2026-03-27 10:27:23 +01:00
Eksperimental cdbf9b7394 Improve function argument naming, specs and guards in Mix (#15216) 2026-03-26 12:18:15 +01:00
Jean Klingler a58a40ce48 Purge and delete modules if after_verify/2 callback fails (#15220) 2026-03-26 20:08:02 +09:00
José Valim a24d3ff221 Avoid false positives when diffing bitstrings, closes #15204 2026-03-25 08:20:05 +01:00
Eksperimental b77e65dd8a Improve funcion signatures and guard consistency in Module (#15215)
Before function signatures showed arguments as: arg, tuple, tuples.
Now they appear as: definition, definitions.

Guards has been improved and standardized, along with variable names.
2026-03-25 08:19:24 +01:00
José Valim 37bbba9d58 Document map_get behaviour, do not split term types, closes #15209 2026-03-25 08:06:50 +01:00
José Valim 2a7a37f598 Add reverse arrows for min/max, closes #15210 2026-03-25 08:00:32 +01:00
José Valim 972df19e13 Work around compiler bug in Erlang/OTP 27+ 2026-03-24 19:50:31 +01:00
José Valim 02deafde99 Avoid warnings in docs 2026-03-24 19:32:57 +01:00
José Valim bafb68db8e Another attempt to work around Erlang/OTP compiler bug 2026-03-24 19:14:50 +01:00
José Valim 24ebb96a27 Expand case by hand to avoid bug in Erlang/OTP 27+ 2026-03-24 19:05:40 +01:00
José Valim 8eb702aea3 Add fast path for two leafs 2026-03-24 18:43:32 +01:00
José Valim 415cd20100 Allow difference formulas to apply across unions 2026-03-24 18:32:33 +01:00
José Valim 5979c42de7 Fix rendering of regex docs 2026-03-24 11:32:51 +01:00
José Valim 5949bad336 Fix pretty printing of map difference 2026-03-23 22:24:00 +01:00
José Valim cfd62cec73 Propagate fields on difference only once, closes #15207 2026-03-23 22:17:50 +01:00
José Valim 447427a141 Update CHANGELOG 2026-03-23 13:02:38 +01:00
José Valim a9ce579829 Revert structs into export time dependencies 2026-03-23 12:52:50 +01:00
José Valim 8df8f92030 Fix introduction to the next topic in some pages, closes #15205 2026-03-22 20:04:55 +01:00
dependabot[bot] b5d66beae7 Bump zizmorcore/zizmor-action from 0.5.0 to 0.5.2 (#15200)
Bumps [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) from 0.5.0 to 0.5.2.
- [Release notes](https://github.com/zizmorcore/zizmor-action/releases)
- [Commits](https://github.com/zizmorcore/zizmor-action/compare/0dce2577a4760a2749d8cfb7a84b7d5585ebcb7d...71321a20a9ded102f6e9ce5718a2fcec2c4f70d8)

---
updated-dependencies:
- dependency-name: zizmorcore/zizmor-action
  dependency-version: 0.5.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-20 19:52:52 +01:00
José Valim e17069e559 Protocol should not add deps on implementations, closes #15202 2026-03-19 10:17:04 +01:00
José Valim 4aa837d255 Remove unused optimization branch in difference 2026-03-18 17:25:08 +01:00
José Valim d8c921fabc Refine hd/tl in guards, closes #15198 2026-03-17 09:36:20 +01:00
José Valim 9c00059f18 Further optimize map differences and fix bug in map_update with negated keys 2026-03-16 23:27:44 +01:00
José Valim 630d5ca629 Fix typos in code comments 2026-03-16 23:27:44 +01:00
Panagiotis Nezis 5bf4df5520 Raise ArgumentError in Keyword.from_keys/2 for non-atom keys (#15197) 2026-03-16 18:30:53 +01:00
João Duarte a7b2d6c679 Fix --warnings-as-errors not catching misnamed test file warnings (#15194) 2026-03-16 18:30:08 +01:00
José Valim 4064905654 Do not perform structural operations on leaf difference 2026-03-15 17:30:50 +01:00
José Valim 6cb9140d9f Remove unnecessary empty checks when computing previous types 2026-03-15 15:31:19 +01:00
José Valim bffb6965a6 Implement one key difference for leaf bdd difference
This allows us to implement previous as a series of
unions, which performs better across all cases.

Closes #15195.
2026-03-15 15:18:47 +01:00
José Valim c39d9ea762 Add more comments to descr, avoid unnecessary traversals 2026-03-15 13:24:03 +01:00
José Valim 98468bd8e3 Further optimize leaf differences 2026-03-15 12:36:35 +01:00
José Valim c8c7abc3b2 Do not check previous if we know args types and previous are disjoint 2026-03-14 19:33:23 +01:00
José Valim 690323e174 Perform fewer args_to_domain conversions 2026-03-14 19:01:19 +01:00
José Valim 528f8e7d7b Return boolean for is_integer/3 with all integers 2026-03-14 11:17:17 +01:00
José Valim 127901b8f8 Add type signature for :erlang.is_integer/3 2026-03-14 11:16:03 +01:00
José Valim aec5d53284 Optimize tuple_fetch, closes #15154 2026-03-14 11:00:40 +01:00
dependabot[bot] 445895c98a Bump actions/attest-sbom from 3.0.0 to 4.0.0 (#15177)
Bumps [actions/attest-sbom](https://github.com/actions/attest-sbom) from 3.0.0 to 4.0.0.
- [Release notes](https://github.com/actions/attest-sbom/releases)
- [Changelog](https://github.com/actions/attest-sbom/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-sbom/compare/4651f806c01d8637787e274ac3bdf724ef169f34...07e74fc4e78d1aad915e867f9a094073a9f71527)

---
updated-dependencies:
- dependency-name: actions/attest-sbom
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-14 09:50:35 +01:00
dependabot[bot] e9adf01e3f Bump actions/upload-artifact from 6.0.0 to 7.0.0 (#15176)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6.0.0 to 7.0.0.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/b7c566a772e6b6bfb58ed0dc250532a479d7789f...bbbca2ddaa5d8feaa63e36b76fdaad77386f024f)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-14 09:50:21 +01:00
José Valim bc7af21c02 Allow overriding specific dependencies in override (#15193)
Closes #14082. Closes #14156.
2026-03-14 09:48:39 +01:00
José Valim 7945446cb4 Allow preserving sigil metadata in container cursor to quoted
Closes #14243.
2026-03-13 20:03:34 +01:00
José Valim 2e7a49d426 Abort diffing on binary patterns, closes #15191 2026-03-13 18:25:07 +01:00
José Valim 5bad452d0a Show undefined function errors even on variable failure, closes #13465 2026-03-13 17:07:37 +01:00
José Valim ec8156aad9 Tiny cleanup on sigil chapter 2026-03-13 11:53:10 +01:00
Jonatan Kłosko 7967ff985b Add :dbg_callback option to Code eval functions (#15189) 2026-03-13 08:13:01 +01:00
Jonatan Kłosko fb2657aa55 Fix compile env change triggering full recompilation of path dependencies (#15188) 2026-03-13 08:11:01 +01:00
Gonzalo R 4021272d42 Fix missing space typo in compiler error message (#15186) 2026-03-12 21:45:00 +01:00
Philipp 28ffbcfe84 Add nil as documented valid option for GenServer :name (#15187) 2026-03-12 20:37:58 +01:00
José Valim f9310524b1 Revert "Fix unnecessary recompilation when dbg_callback is modified at runtime (#15007)" (#15185)
This reverts commit c80a3b1a0e.
2026-03-12 12:50:48 +01:00
José Valim 57b0e29135 Add a build lock around protocol consolidation in umbrellas, closes #15133 2026-03-12 12:45:38 +01:00
José Valim b18637fb97 Treat modules with --no-debug-info as uncached, closes #15159
Additionally make sure --debug-info is part of the cache key.
2026-03-12 11:12:26 +01:00
José Valim 7f3d95b60c Use config files to pass project state mix deps.partition, closes #15181 2026-03-12 10:24:17 +01:00
José Valim df0553bd08 Do not raise false positive on not is_struct checks, closes #15183 2026-03-12 09:59:54 +01:00
José Valim 3452e6ab2f Ensure compilation of sibling deps do not mark path deps as changed, closes #15158 2026-03-11 17:02:00 +01:00
Panagiotis Nezis ababaf8b08 Fix URI.merge leaking :+ marker when base path is empty string (#15179)
When merging URIs where the base has `path: ""`, the internal `:+` marker
used to track the join point between base and relative paths was being
converted to a literal "+" in the output.
2026-03-11 12:07:04 +01:00
José Valim a064a00601 Fix CI and improve ExUnit results 2026-03-11 11:19:51 +01:00
Andrea Leopardi 352bc46c8e Improve ExUnit failure/success output (#15168) 2026-03-11 08:27:58 +01:00
José Valim 74d61833ff Add docs on native coverage 2026-03-11 08:13:53 +01:00
Panagiotis Nezis 9b80ab584d Fix admonition blocks in Module docs (#15175) 2026-03-10 17:56:20 +01:00
Jean Klingler 526d00f949 Always set ERLC_OPTS=warnings_as_errors in CI (#15174) 2026-03-10 22:33:17 +09:00
Guillaume Duboc 51207406d0 Clarify and test gradual function application (#15173)
* Implement gradual function domains and tighten function application

Add fun_domain/1 to compute function domains for static, dynamic, and mixed function types, including explicit :badfun and {:badarity, ...} outcomes when domain extraction is not well-defined.

Refine gradual function application internals by separating static and dynamic normalization paths, documenting behavior for purely dynamic and mixed cases, and clarifying domain/compatibility handling.

Expand descr tests with gradual-application edge cases and a dedicated fun_domain test suite covering static, dynamic, mixed, and arity-error scenarios.

* Refactor function application and domain handling

* Remove fun_domain, recover coverage via fun_apply tests

fun_domain was only used in tests; its behavior is fully observable
through fun_apply (wrong-domain arg → :badarg, right-domain → :ok).
Removed it and converted the domain tests to fun_apply assertions,
adding a few previously uncovered cases along the way: pure non-fun
badfun, union-arg-type enforcement, 2-arity, mixed-arity badarity
for both static and dynamic unions, and the mixed static+dynamic
gradual domain case.

* Remove dead helpers introduced alongside fun_domain

fun_single_arity, fun_single_arity_pair, fun_non_empty_arities_of,
and fun_non_empty_arities are now unreachable. Also revert
dynamic_fun_top? to the simpler map == %{} check — the semantic
emptiness scan added in the refactor was only needed to handle
the same edge cases fun_domain was handling.

* Move mixed-arity badarity check to static side of fun_normalize_both

fun_normalize itself stays simple (no arity cross-check). The check
for other non-empty arities now lives in fun_normalize_both via
fun_other_non_empty_arities/2, and only fires for the static
component. This means:

- union(fun/1, fun/2) applied with 1 arg → {:badarity, [1, 2]}
  (static: we know one branch will always fail at runtime)

- union(dynamic_fun/1, dynamic_fun/2) applied with 1 arg →
  {:ok, dynamic(integer())} (dynamic: picks matching-arity arrows,
  wraps in dynamic() to reflect that the other branch may fail)

* Add tests for mixed-arity union behavior

Static union: badarity regardless of which arity is called with,
with the called arity listed first followed by the others.

Dynamic union: picks the matching-arity arrows and wraps in dynamic(),
gives badarity only when no arity matches at all, and falls back to
dynamic() when the arg is outside the domain but dynamically compatible.
2026-03-10 13:39:54 +01:00
Jean Klingler 78a5151865 Add support for [:raw] opts in File.read (#15172) 2026-03-10 20:07:07 +09:00
Andrea Leopardi c464eb89c5 Add pointer to CLI config in Mix.Task.preferred_cli_env/1 (#15171) 2026-03-10 12:00:22 +01:00
Jean Klingler 10902df940 More cleanup of older OTP versions (#15170)
* More cleanup since we require OTP 27

* Remove mentions to OTP25- in docs

* Use same versions in Getting Started as in Mix guide
2026-03-10 19:49:17 +09:00
José Valim 44e0541df1 Avoid unnecessary concat in tuple elim size 2026-03-10 11:10:58 +01:00
Jean Klingler 71642e381a Add Process.get_label/1 to expose :proc_lib.get_label (#15169) 2026-03-10 19:01:10 +09:00
Panagiotis Nezis d0ce39e1ce Fix incorrect typespecs in Module (#15167)
- Fix `module_info(:nifs)` typespec to return `[function_info]` instead of `module()`
- Fix `Module.overridables_in/1` typespec to return `[definition]` instead of `[atom]`
2026-03-10 10:06:04 +01:00
José Valim e3c74e1e1e Simplify tuple union implementation 2026-03-10 09:48:08 +01:00
dependabot[bot] c184b82b5f Bump actions/download-artifact from 7.0.0 to 8.0.0 (#15126)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7.0.0 to 8.0.0.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/37930b1c2abaa49bbe596cd826c3c89aef350131...70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: 8.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-10 09:01:25 +01:00
José Valim 90b5266ca7 Do not expand negations when pretty printing tuples, see #15154 2026-03-10 08:44:12 +01:00
José Valim 382b5b06de Convert domains to also use orddict 2026-03-10 08:17:00 +01:00
Jean Klingler 2c54f9a64a More cleanup: remove compatibility code for OTP26- (#15166)
* Remove compatibility code for OTP26-

* Remove support for application/erlang+html docs v1

* Remove workaround for bug in older OTP versions

* Update OTP version checks

* Remove OTP25- skipping in tests

* More test cleanup
2026-03-10 16:12:36 +09:00
Jean Klingler 2817a70680 Remove mentions to OTP27+ versions from docs, comments and tests (#15165)
* Remove mentions of "since OTP27+"

* Remove test code for OTP26-
2026-03-10 15:39:03 +09:00
Panagiotis Nezis acd76b68ab Fix PartitionSupervisor.count_children/1 and stop/3 to support via tuples (#15161)
The `name()` type for PartitionSupervisor includes `{:via, module(), term()}`
tuples, but `count_children/1` and `stop/3` only accepted atoms, raising
`FunctionClauseError` for via tuples.
2026-03-10 07:24:37 +01:00
Panagiotis Nezis 6f9afad9ca Fix Integer.extended_gcd/2 returning negative GCD for zero base cases (#15163)
When one argument is 0 and the other is negative, `extended_gcd/2`
was returning a negative GCD, violating both the @spec (which
declares `non_neg_integer`) and consistency with `gcd/2`.
2026-03-10 07:24:29 +01:00
Jean Klingler 13024f43c0 Remove OTP26 support and add OTP29-rc (#15164)
* Update OTP compatibility table

* Add OTP 29.0 rc to CI, remove 26.0

* Remove :elxiir_json polyfill

* Remove :elixir_utils.jaro_similarity polyfill

* defdelegate directly to :proc_lib.set_label/2
2026-03-10 15:10:19 +09:00
Panagiotis Nezis 8914dcdcbb Fix Integer.undigits/2 to validate negative out-of-range digits (#15160)
The validation was asymmetric - it rejected positive digits >= base
but silently accepted negative digits <= -base. This fix adds the
missing check for negative out-of-range digits.
2026-03-09 20:35:10 +01:00
José Valim 2db87ebc70 Release v1.20.0-rc.3 2026-03-09 19:49:28 +01:00
Panagiotis Nezis 5e7671a035 Fix Float.parse/1 inconsistent error handling for non-scientific notation overflow (#15157)
`Float.parse/1` was returning `:error` for overflow in scientific notation (e.g.,
"1.0e400") but raising `ArgumentError` for equivalent overflow in non-scientific
notation (e.g., a 310-digit number). This violated the documented behavior that
states `Float.parse/1` should return `:error` when the float exceeds the maximum
size.
2026-03-09 19:30:26 +01:00
Panagiotis Nezis 52c58c72d3 Preserve directory permissions in File.cp_r/3 (#15155)
The documentation states that `cp_r/3` maintains "modes" when copying, but
directory permissions were not being preserved - only file permissions.
2026-03-09 14:40:00 +01:00
José Valim d56669789f Process fields even when structs are unknown, closes #15156 2026-03-09 14:38:54 +01:00
José Valim 92e1830bf7 Continue traversing tuple leafs only if subtyping is a possibility 2026-03-09 14:07:56 +01:00
José Valim 4468a12222 Do not compute previous subtype during inference 2026-03-09 12:11:55 +01:00
Panagiotis Nezis c6820a1297 Fix File.cp_r/3 infinite loop with symlink cycles (#15152)
When `dereference_symlinks: true` was set and there was a symlink cycle
(e.g., a -> b -> a), the function would infinitely recurse. This fix
tracks visited paths using a `MapSet` and returns `{:error, :eloop, path}`
when a cycle is detected.
2026-03-09 10:42:29 +01:00
José Valim 93734a5c86 Provide better tuple elimitation in differences 2026-03-09 09:57:46 +01:00
José Valim e5811283af Avoid double negation removal pass on map pretty printing 2026-03-09 09:11:06 +01:00
José Valim 2639028da8 Do not always compute one key difference in map difference 2026-03-09 09:03:42 +01:00
José Valim 9add53318a Avoid cascading unions in map difference 2026-03-09 08:56:29 +01:00
José Valim 27aadff38d Tackle empty intersections in map fetch key 2026-03-09 08:20:45 +01:00
José Valim 64a5c1742f Document why binary_part operation is safe 2026-03-08 23:46:17 +01:00
José Valim b98d823c54 Simplify differences (#15153)
The previous implementation was incorrectly nesting differences.
So if we had four clauses, the four clause was computing the type as:

    clause4 - (clause3 - clause2 - clause1) - (clause2 - clause1) - clause1

While it should have been:

    clause4 - clause3 - clause2 - clause1

This means that instead of `n` differences, we had an arithmetic progression
of `n * (n + 1) / 2` differences.
2026-03-08 23:02:28 +01:00
Panagiotis Nezis ea672ccd81 Fix spec of PartitionSupervisor.partitions/1 (#15151) 2026-03-08 22:53:51 +01:00
José Valim 4ad3e0afb9 Fix map_difference_strategy with optional keys 2026-03-08 11:38:24 +01:00
José Valim a5e83ebc04 Use separate strategy function for differences 2026-03-08 11:03:51 +01:00
Panagiotis Nezis dd1f52917c Fix File.cp_r/3 infinite loop when copying into subdirectory of source (#15148)
When destination is a subdirectory of source, `cp_r` would enter an
infinite loop because the newly created destination directory would
be included in the file listing during recursion. This can **cause disk
exhaustion** if large files are present under source.

This adds a validation check that returns `{:error, :einval, destination}`
when copying on a subdir of the source path.

Also document typical error reasons including the new `:einval` case.
2026-03-08 11:03:10 +01:00
Jean Klingler 07e6e1a985 Warn when defining @type record(), fixes CI on OTP29 (#15150) 2026-03-08 17:10:16 +09:00
José Valim 663c4ab57e Make type of map_bdd_to_dnf conversion explicit 2026-03-07 23:05:13 +01:00
José Valim 0dbdadae4a Revert previous strategy to use multiple differences 2026-03-07 22:59:55 +01:00
José Valim d4495ae22a Use different strategy to remove negations in map access 2026-03-07 22:58:30 +01:00
José Valim 09c90ee5fc Reuse map_leaf_strategy on difference 2026-03-07 22:58:30 +01:00
Panagiotis Nezis bbe700151e Fix File.Stream Enumerable.count for files without trailing newline (#15147)
The optimized count implementation was counting newline characters
instead of actual lines, returning incorrect results for files that
don't end with a newline (which is common for source code files).

For example, a file with content "line1\nline2\nline3" (3 lines)
was returning 2 instead of 3.

The fix tracks the last byte read and adds 1 at EOF if the file
has content but doesn't end with a newline.
2026-03-07 11:54:01 +01:00
Panagiotis Nezis d61ba915b0 Raise ArgumentError on invalid binding in Code.eval_string/3 (#15146)
Previously, passing a non-list binding to `Code.eval_string/3` would crash
deep in Erlang code with a confusing `FunctionClauseError` in
`:elixir_erl_var.load_binding/6`.
2026-03-07 10:01:59 +01:00
Daniel Kukula 2973af6c97 Filter modules during collection in IEx autocomplete (#15143)
Instead of collecting all modules as strings, sorting the full list, and
then linearly scanning for matching prefixes, filter modules during
collection and only sort the (much smaller) set of matches.

Benchmarked with 261 loaded modules, compared against the original code
(before the previous two optimizations) and the current code:

  match_modules("Elixir.Enum"):
    before PR:  333 µs, 13.6 KB heap
    current:     91 µs,  1.6 KB heap
    proposed:    30 µs,  0.5 KB heap  (11x faster, 26x less heap)

  match_modules("er"):
    before PR:  345 µs, 13.8 KB heap
    current:     97 µs,  2.9 KB heap
    proposed:    39 µs,  0.4 KB heap  (9x faster, 33x less heap)
2026-03-07 09:12:36 +01:00
Panagiotis Nezis 47d72d94d8 Fix Enum.slice/2 for ranges with step > 1 sliced by step > 1 (#15144)
When slicing a Range with step > 1 using a slice range that also has
step > 1, the formula for calculating the step in the slicer function
was incorrect.
2026-03-07 08:56:13 +01:00
José Valim 9a663cf3fa Convert map fields into an orddict and implement meet optimizations (#15142)
Related to #15129.
2026-03-06 21:57:55 +01:00
Daniel Kukula 7d3863fcf6 Reduce memory usage in IEx autocomplete module listing (#15140)
Use :erlang.loaded/0 instead of :code.all_loaded/0 since we only need
module names, not file paths. The latter copies the full path for every
loaded module, which on production systems with many dependencies
caused significant memory spikes (measured 2.8MB -> 172KB on a real pod).

Also replace Enum.sort/1 |> Enum.dedup/1 with :lists.usort/1 and
avoid list allocations.
2026-03-06 19:20:41 +01:00
Raúl R Pearson 29f572643e Upcase "PID" in Task docs (#15141) 2026-03-06 17:36:07 +01:00
Eric Meadows-Jönsson 80df1fe3dc Fix undefined ref_name variable in release S3 upload (#15139) 2026-03-06 12:15:42 +01:00
José Valim 6fd161d67d Optimize previous handling by using nested unions instead of nested differences
Partially addresses #15129.
2026-03-05 12:19:47 +01:00
José Valim 576f123b16 Intersection of maps with not set keys should not be empty, closes #15131 2026-03-05 10:34:03 +01:00
José Valim 77e6687eef Handle Kernel.in/2 in type system instead of using rewrite rules, closes #15130 2026-03-05 08:48:03 +01:00
José Valim de4b1610a9 Update comment on square brackets validity in WHATWG 2026-03-04 21:43:02 +01:00
José Valim 0df45fdba1 Release v1.20.0-rc.2 2026-03-04 16:16:54 +01:00
José Valim 8e5e62653e More docs and warnings around URLs, closes #15125 2026-03-04 09:48:10 +01:00
José Valim f7c4047d30 Keep old functions around so old artifacts still work as expected 2026-03-03 13:37:27 +01:00
José Valim 6e6ed6a3b5 Only return purgeable modules back to pool 2026-03-03 13:35:09 +01:00
José Valim 0ea809e720 Check redundant clauses for definitions (#15124) 2026-03-02 17:19:51 +01:00
Jean Klingler cd38841a18 Raise clearer error when reserved word used at the top level of defmodule (#15123) 2026-03-02 18:18:35 +09:00
Jean Klingler 132197a254 Remove redundant clause triggering warning in tests (#15122) 2026-03-02 17:17:07 +09:00
George Guimarães 8cda8e1bae Print formatted file names in mix format --verbose (#15119) 2026-02-28 12:25:38 +01:00
José Valim 978e4047f4 Update CHANGELOG 2026-02-27 15:46:20 +01:00
José Valim b792fb41c5 Clarify error message for always failing private function, closes #14401 2026-02-27 15:16:02 +01:00
José Valim d7865ecfa7 Improve inference for anonymous functions 2026-02-27 15:02:09 +01:00
José Valim 1f211d121e Clean up diagnostic formatting on patterns 2026-02-27 12:20:08 +01:00
José Valim aed240bf40 Warns on redundant clauses from fn 2026-02-27 11:12:22 +01:00
José Valim bac6ef9d30 Implement redundant clauses for reduce and convert them to warnings 2026-02-27 10:42:13 +01:00
José Valim 7b0ae43231 Address ambiguity on type violation in nested || 2026-02-27 08:56:22 +01:00
Eksperimental 677afeced3 Improve typespecs for t:ExUnit.configure_opts/0 (#15117)
`:exclude`/`:include` can be:
- an atom
- a list of atoms and keyword elements
2026-02-27 08:48:20 +01:00
Eksperimental 2292f3bcb6 Improve docs for Filters in ExUnit (:exclude and :include configuration) (#15118) 2026-02-27 08:47:54 +01:00
José Valim a2a669eb7b Improve error messages in many Kernel operations 2026-02-26 17:58:18 +01:00
José Valim b85d1c84d3 Print negations whenever most of the descr types are either top or negated 2026-02-26 17:58:18 +01:00
Jonatan Männchen 4602240192 Add Software Bill of Materials guide to the Documentation (#15067)
Introduces a new guide explaining SBoMs: what they are, why they matter
(vulnerability analysis, regulatory requirements, license compliance),
and how to generate them for Elixir projects using mix_sbom. Also covers
CI integration via GitHub Action and mentions ORT for deeper file-level
license scanning.
2026-02-26 10:07:12 +01:00
Jonatan Männchen a71a4e8f7a Keep credentials for release branch tagging (#15115)
The checkout in create_draft_release needs credentials to push
the branch-latest tag to origin.
2026-02-25 13:16:23 +01:00
Jonatan Männchen af34af86aa Add zizmor workflow audit to CodeQL workflow
Run zizmor static analysis on GitHub Actions workflows to detect
potential security vulnerabilities in CI/CD configuration.

See: https://docs.zizmor.sh/
2026-02-25 12:59:01 +01:00
Jonatan Männchen 920b7bef2e Add CodeQL workflow for GitHub Actions security analysis
Run CodeQL static analysis on GitHub Actions workflows to detect
potential security vulnerabilities in CI/CD configuration.

Scheduled weekly and on pushes/PRs to main branch.
2026-02-25 12:59:01 +01:00
Jonatan Männchen 01212cefc8 Suppress false-positive github-env warnings from zizmor
The GITHUB_PATH and GITHUB_ENV writes are safe as they use only
workflow-controlled values, not attacker-controlled input.

See: https://docs.zizmor.sh/audits/#github-env
2026-02-25 12:59:01 +01:00
Jonatan Männchen 31d62fddb5 Add cooldown to Dependabot configuration
Delay automatic dependency updates to allow time for discovery of
regressions or supply-chain compromises in newly released versions.

See: https://docs.zizmor.sh/audits/#dependabot-cooldown
2026-02-25 12:59:01 +01:00
Jonatan Männchen 32d20fd77c Prevent template injection in GitHub Actions workflows
Use environment variables instead of direct template expansion
in shell run blocks to prevent potential code injection.

See: https://docs.zizmor.sh/audits/#template-injection
2026-02-25 12:59:01 +01:00
Jonatan Männchen dfe857e7df Prevent credential persistence in checkout actions
Add `persist-credentials: false` to all `actions/checkout` usages
to prevent Git credentials from being persisted in the repository
after checkout completes.

See: https://docs.zizmor.sh/audits/#artipacked
2026-02-25 12:59:01 +01:00
Michał Łępicki 6d5f43318c Allow specifying SEED when running tests (#15111)
this is helpful when reproducing flaky test failures
2026-02-25 09:12:15 +01:00
Michał Łępicki 43dde95ed2 Update ParallelCheker.verify/2 spec (#15113)
The return type changed in https://github.com/elixir-lang/elixir/commit/a0ef7f0c3e0049468799ffcea5dddb6542a01568
2026-02-24 23:07:24 +01:00
Michał Łępicki 565f468d68 Clean up unused in_body? code branch in in macro impl (#15112)
The dialyzer warning was:

lib/kernel.ex:4836:7: Guard test _in_body?@1::'false' =:=
          'true' can never succeed
2026-02-24 21:19:59 +01:00
José Valim c903033097 Compute redundant clauses for try-catch and try-else 2026-02-24 13:11:05 +01:00
José Valim 7caa954690 Compute redundant clauses for receive 2026-02-24 11:19:06 +01:00
José Valim 4c203429d6 Include maybe feature 2026-02-23 14:27:56 +01:00
José Valim a0ef7f0c3e Move struct validation in patterns and updates to type checker
This means changing a struct definition only cases
recompilation when structs are created, no longer on
matches and updates.

Closes #15098.
2026-02-23 14:24:29 +01:00
José Valim 34a50002f9 Keep Kernel.in/2 as :lists.member/2 and optimize in type checker and Erlang pass 2026-02-23 14:24:29 +01:00
Łukasz Jan Niemier 38dcc453b8 Optimise Map.take/2 for empty list of keys (#15110) 2026-02-23 09:10:09 +01:00
José Valim 34fda89acb Recompose multiple orelse as Kernel.in for performance, closes #15108 2026-02-20 18:54:45 +01:00
Steffen Deusch 13846bd25f Fail when misnamed test files are detected and --warnings-as-errors is set (#15106)
Closes https://github.com/elixir-lang/elixir/issues/15105.
2026-02-19 18:56:33 +01:00
José Valim b05fec0c87 Use evaluation when compiling modules (#15087) 2026-02-19 18:11:28 +01:00
José Valim 9ba89f4da6 Properly handle compile attributes in Erlang sources 2026-02-18 13:53:24 +01:00
José Valim 85b1ad0764 Do not emit warnings during IEx parsing
Closes #15090.

Unfortunately, I could not write a test for this as the error
only happens when using actual I/O devices. When using the mock
device we use in tests, the warning appears as expected.
2026-02-18 13:45:56 +01:00
José Valim 23633f7723 Use IEx.Broker.shell to find nodes instead of group leader, closes #15092 2026-02-18 12:15:22 +01:00
José Valim ab7c019d24 Topsort Erlang modules before compilation, closes #15100 2026-02-18 10:59:32 +01:00
José Valim f2e61f6a04 Use system tmp dir for socket copy test, closes #15093 2026-02-18 09:59:23 +01:00
José Valim 5abcc33267 Improve error message and docs for headless clauses, closes #15104 2026-02-18 09:54:04 +01:00
José Valim 52296e00c6 Improve case error message and improve performance 2026-02-18 09:36:52 +01:00
dependabot[bot] d8eed7812b Bump azure/trusted-signing-action from 1.0.0 to 1.1.0 (#15103)
Bumps [azure/trusted-signing-action](https://github.com/azure/trusted-signing-action) from 1.0.0 to 1.1.0.
- [Release notes](https://github.com/azure/trusted-signing-action/releases)
- [Commits](https://github.com/azure/trusted-signing-action/compare/db7a3a6bd3912025c705162fb7475389f5b69ec6...87c2e83e6868da99d3380aa309851b32ed9a8346)

---
updated-dependencies:
- dependency-name: azure/trusted-signing-action
  dependency-version: 1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-16 23:23:45 +01:00
Simon Hohl 2ab126e223 Add another example to Integer.parse/1 (#15101) 2026-02-12 15:18:54 +01:00
Alex NaserandAlex Naser f2e6d6a2fb Parallelize dep lock status checks during deps.loadpaths (#15099)
To make this safe, git.ex's lock_status no longer uses File.cd!
(which mutates global process state), instead passing the `:cd` into System.cmd.

Co-authored-by: Alex Naser <alex.naser@remote.com>
2026-02-09 12:49:43 +01:00
José Valim a5361436c4 Do not propagate parent config when building archives/escripts (#15097)
Closes #15094.
2026-02-07 18:47:13 +01:00
наб 0b1d3fcaf9 Improve Application.get_env/2 docs for dependencies
Followup-for: #15095
Signed-off-by: José Valim <jose.valim@gmail.com>
2026-02-06 20:42:12 +01:00
José Valim f7b179ac1d Ensure system_env is applied for Elixir deps, closes #15095 2026-02-05 23:41:12 +01:00
José Valim 426a4297c9 Fix wrong bdd leaf intersection optimization on equal literals, closes #15091 2026-02-03 14:30:29 +01:00
José Valim be34850da4 Remove incorrect differences in BDD 2026-02-02 23:02:47 +01:00
José Valim 3fbe977035 Validate Macro.Env struct upfront to avoid failures deep within the compiler, closes #15088 2026-02-01 20:49:59 +01:00
José Valim cebc5a76fb Better error message for when all patterns are exhausted 2026-02-01 08:45:06 +01:00
José Valim c40c140e3f Postpone deletion of modules to avoid code server bottleneck 2026-01-31 13:56:15 +01:00
José Valim 7fef1c58e4 Make compiler module purging opt-in
We run it before module verification but,
as module verification now loads many modules,
that was negatively impacting compilation times.
2026-01-31 13:56:15 +01:00
Mudassar 7048f60539 Fix spelling typos in docs and comments (#15083) 2026-01-30 08:22:19 +09:00
José Valim 4940d0f99e Reduce state tracked in guard info 2026-01-29 18:23:52 +01:00
Pierre Le Gall 7b22b8e7eb Add List.first!/1 and List.last!/1 (#15082) 2026-01-29 17:23:40 +01:00
Andrew France ba86bebd4c Fix "always evaluate" for Logger anonymous function tuples (#15081)
Previously, when the configuration directive `always_evaluate_messages`
was set to true an exception was raised when a function that returns a
tuple was given to Logger:

  ```
  Logger.info(fn -> {"", []} end)
  ```

Would result in:

  ```
    ** (Protocol.UndefinedError) protocol String.Chars not implemented for Tuple

    Got value:

        {"", []}
  ```
2026-01-28 09:35:08 +01:00
José Valim 5093bab3ab Document GenServer shutdown default 2026-01-28 08:54:07 +01:00
José Valim b381ab3ae4 Add require note to CHANGELOG, closes #15077 2026-01-27 19:57:16 +01:00
José Valim 30b59edf39 Track types across case clauses (#15080) 2026-01-27 19:49:08 +01:00
dependabot[bot] ff132444ca Bump actions/checkout from 6.0.1 to 6.0.2 (#15078)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.1 to 6.0.2.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/8e8c483db84b4bee98b60c0593521ed34d9990e8...de0fac2e4500dabe0009e67214ff5f5447ce83dd)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-27 07:58:52 +01:00
José Valim d031425cbd Track guard precision 2026-01-26 18:44:47 +01:00
José Valim dc7b20fe0a Track pattern precision 2026-01-26 16:05:32 +01:00
José Valim 47d2b6d305 Collapse expressions to improve trace debugging 2026-01-26 14:16:21 +01:00
José Valim 663019d048 Clarify tasks are asynchronous on supervisor init 2026-01-24 19:12:28 +01:00
José Valim 73adf727f9 Do not leak variables from conditional vars, closes #15074 2026-01-23 21:10:10 +01:00
José Valim 22006fd77a Track variables across equality in guards 2026-01-23 15:56:49 +01:00
José Valim e87b9359c4 Streamline subpatterns for domain keys and list heads 2026-01-22 19:03:35 +01:00
José Valim b6a0097226 Properly track types in lists in pattern matching, closes #15040 2026-01-22 16:54:16 +01:00
José Valim cbe3d72666 Warn if default arguments are given in protocol definitions, closes #15071 2026-01-22 14:04:24 +01:00
José Valim 44abf19c97 Generate markdown docs for Elixir 2026-01-20 17:02:56 +01:00
dependabot[bot] a26c321e79 Bump azure/trusted-signing-action from 0.5.11 to 1.0.0 (#15072)
Bumps [azure/trusted-signing-action](https://github.com/azure/trusted-signing-action) from 0.5.11 to 1.0.0.
- [Release notes](https://github.com/azure/trusted-signing-action/releases)
- [Commits](https://github.com/azure/trusted-signing-action/compare/1d365fec12862c4aa68fcac418143d73f0cea293...db7a3a6bd3912025c705162fb7475389f5b69ec6)

---
updated-dependencies:
- dependency-name: azure/trusted-signing-action
  dependency-version: 1.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-20 09:00:13 +01:00
Jean Klingler 32082d1367 Add ready to copy example in test_ignore_filters doc for frequent use case (#15070) 2026-01-18 17:48:36 +09:00
Sofía Bordagorry 45734fe216 Fix minor typos in docs and comments (#15068) 2026-01-17 05:19:29 +09:00
José Valim ebfebede92 Fix typo in 'a implementation' 2026-01-16 12:56:53 +01:00
José Valim 8def030f38 Use type warning instead of typing violation, closes #15065 2026-01-16 08:37:04 +01:00
Jean Klingler 61d82a94f8 More fixes and tests for inlined struct opacity issues (#15063)
* Fix doc -> typedoc

* Add regression tests for dialyzer opacity

* Do not use opaque :sets.set in MapSet.t()
2026-01-16 11:50:34 +09:00
José Valim 1b0dff7e6a Do not warn on defaults from defoverridable, closes #15061 2026-01-15 10:16:35 +01:00
José Valim 0862491255 Add type checking for :erlang.map_get/2 to improve is_struct precision 2026-01-15 09:54:43 +01:00
José Valim 19cddfeea1 Use Map.to_list instead of Enum.to_list, closes #15064 2026-01-15 09:48:02 +01:00
José Valim b523a8e706 Simplify struct error message as it won't be deprecated 2026-01-15 09:48:02 +01:00
Jean Klingler 3b75c46513 Do not use opaque type for URI authority (#15062) 2026-01-15 00:29:52 +09:00
José Valim 3bb5f92fa9 Release v1.20.0-rc.1 2026-01-13 11:46:20 +01:00
dependabot[bot] cb472c10f3 Bump actions/checkout from 6.0.0 to 6.0.1 (#15059)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.0 to 6.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...8e8c483db84b4bee98b60c0593521ed34d9990e8)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-13 08:53:37 +01:00
José Valim e5dc69398e Only perform eager literal intersection when maps are closed, closes #15045 2026-01-13 01:07:19 +01:00
José Valim fd6c6af81d Simplify bdd_difference code to discard differences of negations 2026-01-13 00:36:43 +01:00
José Valim a9a0969c28 Also optimize different between structs 2026-01-12 12:17:25 +01:00
José Valim dd284bbf9b Reduce travervals in map_all_but_one 2026-01-12 12:04:48 +01:00
José Valim bc1a862c93 Update CHANGELOG, closes #15058 2026-01-12 09:22:05 +01:00
José Valim ae383313f6 Deal with struct keys in map updates with atom domains
Closes #15055
2026-01-11 22:31:48 +01:00
José Valim f00cb3a9a0 Properly handle bitstrings inside guards, closes #15052 2026-01-11 16:52:34 +01:00
José Valim 9af054f9bc Add types for :erlang.split_binary/2 2026-01-10 22:13:34 +01:00
José Valim 61f55052fe Optimize tuple difference by checking for disjoint tagged tuples 2026-01-10 22:06:23 +01:00
José Valim d8322e3f76 Optimize bdd differences of disjoint literals (#15050)
We implement it for tuples as there is a very cheap disjoint check
2026-01-10 21:44:39 +01:00
José Valim f2ff3a7a86 More docs 2026-01-10 19:53:21 +01:00
José Valim 7f5e4e3bb2 Keep gradual mode in bitstrings 2026-01-10 19:20:37 +01:00
José Valim bfa154e70b Consider subtyping on bitstring segments 2026-01-10 19:12:19 +01:00
José Valim e586ed519e Optimize variable propagation by only visiting nodes once and avoiding immediate back propagation 2026-01-10 17:47:36 +01:00
José Valim 8767c5b530 Properly unpack map fields in descr before enum operations, closes #15049 2026-01-10 16:35:26 +01:00
José Valim 7c13e99412 Do not use opaque in MapSet, closes #14837 2026-01-10 16:15:46 +01:00
José Valim d8ed2b9ae4 Run license compliance on the same workflows as CI 2026-01-10 11:16:13 +01:00
Eksperimental d653d1522a CI: Move license compliance to its own file (#14982) 2026-01-10 11:15:36 +01:00
José Valim 1752058872 Do not crash on conditional variables inside multi-module apply
Closes #15044.
Closes #15046.
2026-01-10 11:05:22 +01:00
409 changed files with 26426 additions and 11350 deletions
+12 -7
View File
@@ -3,8 +3,7 @@
---
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:
@@ -13,11 +12,17 @@ body:
Please, do not use this form for guidance, questions or support.
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.
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
- type: textarea
id: elixir-and-otp-version
+2
View File
@@ -7,3 +7,5 @@ updates:
directory: "/"
schedule:
interval: "weekly"
cooldown:
default-days: 7
+4
View File
@@ -0,0 +1,4 @@
* Describe here the reasons behind the pull request.
* Make sure you have read the CONTRIBUTING.md file.
* Make sure any relevant documentation and tests have been added/updated.
* Do not submit Draft pull requests unless previously asked/agreed.
+14 -40
View File
@@ -26,31 +26,27 @@ jobs:
fail-fast: false
matrix:
include:
- otp_version: "28.1"
- otp_version: "29.0"
deterministic: true
- otp_version: "28.1"
erlc_opts: "warnings_as_errors"
- otp_version: "28.4"
docs: true
coverage: true
- otp_version: "28.1"
- 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
# 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: ${{ matrix.erlc_opts || '' }}
ERLC_OPTS: "warnings_as_errors"
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: erlef/setup-beam@e6d7c94229049569db56a7ad5a540c051a010af9 # v1.20.4
- uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1
with:
otp-version: ${{ matrix.otp_version }}
@@ -96,7 +92,7 @@ jobs:
- name: "Upload Coverage Artifact"
if: ${{ matrix.coverage }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: TestCoverage
path: cover/*
@@ -119,17 +115,19 @@ jobs:
strategy:
matrix:
otp_version:
- "29.0"
- "28.1"
- "27.3"
- "26.2"
steps:
- name: Configure Git
run: git config --global core.autocrlf input
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: erlef/setup-beam@e6d7c94229049569db56a7ad5a540c051a010af9 # v1.20.4
- uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1
with:
otp-version: ${{ matrix.otp_version }}
@@ -151,27 +149,3 @@ jobs:
run: |
Remove-Item 'c:/Windows/System32/drivers/etc/hosts'
make test_elixir
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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- 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
@@ -0,0 +1,55 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2026 The Elixir Team
name: "CodeQL Advanced"
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
schedule:
- cron: "29 8 * * 1"
permissions:
contents: read
jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: "ubuntu-latest"
permissions:
security-events: write
strategy:
fail-fast: false
matrix:
include:
- language: actions
build-mode: none
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
with:
category: "/language:${{matrix.language}}"
zizmor:
name: Zizmor
runs-on: ubuntu-latest
permissions:
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Run zizmor
uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2
+41
View File
@@ -0,0 +1,41 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
name: License Compliance
on:
push:
pull_request:
workflow_dispatch:
permissions:
contents: read
env:
LANG: C.UTF-8
jobs:
license_compliance:
name: Check License Compliance
runs-on: ubuntu-24.04
steps:
- name: Use HTTPS instead of SSH for Git cloning
id: git-config
shell: bash
run: git config --global url.https://github.com/.insteadOf ssh://git@github.com/
- name: Checkout project
id: checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Run OSS Review Toolkit
id: ort
uses: ./.github/workflows/ort
with:
upload-reports: true
fail-on-violation: true
report-formats: "WebApp"
version: "${{ github.sha }}"
+4 -2
View File
@@ -33,7 +33,9 @@ jobs:
fail-fast: false
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Run markdownlint-cli2
uses: DavidAnson/markdownlint-cli2-action@07035fd053f7be764496c0f8d8f9f41f98305101 # v22.0.0
uses: DavidAnson/markdownlint-cli2-action@21c1be1b93ad9ed58fa840aacc3f279cde2a72ff # v24.2.0
+1 -1
View File
@@ -74,6 +74,6 @@ unless System.get_env("DRYRUN") do
"api-username" => "Elixir"
}
resp = Req.post!("https://elixirforum.com/posts.json", {:json, post}, headers: headers)
resp = Req.post!("https://forum.elixirforum.com/posts.json", {:json, post}, headers: headers)
IO.puts("#{resp.status} Elixir Forum\n#{inspect(resp.body)}")
end
+5 -2
View File
@@ -46,6 +46,7 @@ runs:
repository: oss-review-toolkit/ort-config
ref: "main"
path: ".ort-config"
persist-credentials: false
- name: Setup ORT Config
id: setup-ort-config
@@ -88,9 +89,9 @@ runs:
- name: Run OSS Review Toolkit
id: ort
uses: oss-review-toolkit/ort-ci-github-action@1805edcf1f4f55f35ae6e4d2d9795ccfb29b6021 # v1.1.0
uses: oss-review-toolkit/ort-ci-github-action@086d928d24ef1653dc0777296b312fda5faaaf52 # v1.2.0
with:
image: ghcr.io/oss-review-toolkit/ort-minimal:65.0.0
image: ghcr.io/oss-review-toolkit/ort:92.2.0
run: >-
labels,
cache-dependencies,
@@ -106,4 +107,6 @@ runs:
ort-cli-report-args: >-
-O CycloneDX=output.file.formats=json,xml
-O SpdxDocument=outputFileFormats=JSON,YAML
ort-cli-scan-args: >-
--scanners Provenant
sw-version: "${{ inputs.version }}"
+3 -1
View File
@@ -32,7 +32,9 @@ jobs:
fail-fast: false
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install ShellCheck
run: |
+53 -46
View File
@@ -37,30 +37,31 @@ jobs:
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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# zizmor: ignore[artipacked]
if: github.ref_type == 'branch'
- 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)' || '' }}
@@ -70,9 +71,6 @@ jobs:
fail-fast: true
matrix:
include:
- otp: 26
otp_version: "26.0"
- otp: 27
otp_version: "27.0"
@@ -80,8 +78,13 @@ jobs:
otp_version: "28.0"
build_docs: build_docs
- otp: 29
otp_version: "29.0"
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: "Build Release"
uses: ./.github/workflows/release_pre_built
@@ -97,19 +100,19 @@ jobs:
shasum -a 256 Docs.zip > Docs.zip.sha256sum
- name: "Upload Linux release artifacts"
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: build-linux-elixir-otp-${{ matrix.otp }}
path: elixir-otp-${{ matrix.otp }}.zip
- name: "Upload Windows release artifacts"
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: build-windows-elixir-otp-${{ matrix.otp }}
path: elixir-otp-${{ matrix.otp }}.exe
- name: "Upload doc artifacts"
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: matrix.build_docs
with:
name: Docs
@@ -122,7 +125,7 @@ jobs:
strategy:
fail-fast: true
matrix:
otp: [26, 27, 28]
otp: [27, 28, 29]
flavor: [windows, linux]
env:
@@ -136,20 +139,20 @@ jobs:
steps:
- name: "Download build"
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
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@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
uses: azure/login@f5d393ae46f8fde4be8b75f32e3fc50e654ad0ca # v3.0.1
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@1d365fec12862c4aa68fcac418143d73f0cea293 # v0.5.11
uses: azure/trusted-signing-action@c7ab2a863ab5f9a846ddb8265964877ef296ee82 # v2.0.0
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
with:
endpoint: https://eus.codesigning.azure.net/
@@ -179,7 +182,7 @@ jobs:
shasum -a 256 "$RELEASE_FILE" > "${RELEASE_FILE}.sha256sum"
- name: "Upload Linux release artifacts"
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: sign-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
path: ${{ env.RELEASE_FILE }}*
@@ -202,11 +205,13 @@ jobs:
- name: Checkout project
id: checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: "Download Build Artifacts"
id: download-build-artifacts
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: "{sign-*-elixir-otp-*,Docs}"
merge-multiple: true
@@ -221,7 +226,7 @@ jobs:
- name: Attest Distribution Assets with SBoM
id: attest-sbom
uses: actions/attest-sbom@4651f806c01d8637787e274ac3bdf724ef169f34 # v3.0.0
uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0
with:
subject-path: |
/tmp/build-artifacts/{elixir-otp-*.*,Docs.zip}
@@ -241,15 +246,19 @@ jobs:
cp "$ATTESTATION" "attestations/$(basename "$FILE").sigstore"
done
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"
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"
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@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: "SBoM"
path: |
@@ -259,7 +268,7 @@ jobs:
${{ steps.ort.outputs.results-sbom-spdx-json-path }}
- name: "Assemble Distribution Attestations"
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: "Attestations"
path: "attestations/*.sigstore"
@@ -273,7 +282,7 @@ jobs:
contents: write
steps:
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: "{sign-*-elixir-otp-*,Docs,SBoM,Attestations}"
merge-multiple: true
@@ -283,14 +292,14 @@ jobs:
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 \
@@ -324,7 +333,7 @@ jobs:
echo "Required variables for uploading to hex.pm are not set up, skipping..."
exit 1
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: "{sign-*-elixir-otp-*,Docs}"
merge-multiple: true
@@ -335,12 +344,10 @@ 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/${ref_name}}
dest=${zip/elixir/${GITHUB_REF_NAME}}
surrogate_key=${dest/.zip$/}
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${dest}" \
@@ -349,16 +356,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/${ref_name}.zip" \
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${GITHUB_REF_NAME}.zip" \
--cache-control "public,max-age=3600" \
--metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${ref_name}\",\"surrogate-control\":\"public,max-age=604800\"}"
echo builds/elixir/${ref_name} >> purge_keys.txt
--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
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
@@ -379,7 +386,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)
+5 -3
View File
@@ -17,9 +17,11 @@ jobs:
name: Notify
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: erlef/setup-beam@e6d7c94229049569db56a7ad5a540c051a010af9 # v1.20.4
- uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1
with:
otp-version: "27.3"
elixir-version: "1.18.3"
@@ -29,4 +31,4 @@ jobs:
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"
+13 -7
View File
@@ -25,10 +25,12 @@ runs:
- name: Build Elixir Release
shell: bash
run: |
run: | # zizmor: ignore[github-env]
make Precompiled.zip
mv Precompiled.zip elixir-otp-${{ inputs.otp }}.zip
mv Precompiled.zip "elixir-otp-${INPUT_OTP}.zip"
echo "$PWD/bin" >> $GITHUB_PATH
env:
INPUT_OTP: ${{ inputs.otp }}
- name: Install NSIS
shell: bash
@@ -39,15 +41,18 @@ runs:
- name: Build Elixir Windows Installer
shell: bash
run: |
export OTP_VERSION=${{ inputs.otp_version }}
export ELIXIR_ZIP=$PWD/elixir-otp-${{ inputs.otp }}.zip
export OTP_VERSION="$INPUT_OTP_VERSION"
export ELIXIR_ZIP="$PWD/elixir-otp-${INPUT_OTP}.zip"
(cd lib/elixir/scripts/windows_installer && ./build.sh)
mv lib/elixir/scripts/windows_installer/tmp/elixir-otp-${{ inputs.otp }}.exe .
mv "lib/elixir/scripts/windows_installer/tmp/elixir-otp-${INPUT_OTP}.exe" .
env:
INPUT_OTP: ${{ inputs.otp }}
INPUT_OTP_VERSION: ${{ inputs.otp_version }}
- name: Get ExDoc ref
if: ${{ inputs.build_docs }}
shell: bash
run: |
if [ "${{ github.ref_name }}" = "main" ]; then
run: | # zizmor: ignore[github-env]
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')
@@ -59,6 +64,7 @@ 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 -1
View File
@@ -5,7 +5,8 @@
"**/*.md"
],
"ignores": [
".git/**"
".git/**",
".github/**"
],
"gitignore": true,
"config": {
+5
View File
@@ -37,6 +37,11 @@ curations:
comment: "Apply Trademark Policy to VERSION file"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: ".github/pull_request_template.md"
reason: "NOT_DETECTED"
comment: "Apply default license to GitHub pull request template"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Wrongly Identified
- path: ".gitignore"
Symlink
+1
View File
@@ -0,0 +1 @@
CONTRIBUTING.md
+88 -177
View File
@@ -4,197 +4,108 @@
SPDX-FileCopyrightText: 2012 Plataformatec
-->
# Changelog for Elixir v1.20
# Changelog for Elixir v1.21
## Type system improvements
This release includes type inference of all constructs.
### Type inference of function calls
Elixir now performs inference of whole functions. The best way to show the new capabilities are with examples. 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.
### Type inference of guards
This release also performs inference of guards! Let's see some examples:
```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).
### Complete typing of maps keys
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(%{})
end
end
```
The code above has a type violation, which is now caught by the type system:
```text
warning: incompatible types given to User.name/1:
User.name(%{})
given types:
%{name: not_set()}
but expected one of:
dynamic(%{..., name: term()})
typing violation found at:
│
16 │ User.name(%{})
│ ~
│
└─ lib/calls_user.ex:7:5: CallsUser.calls_name/0
```
### 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/).
## v1.20.0-rc.0 (2026-01-09)
## v1.21.0-dev
### 1. Enhancements
#### EEx
* [EEx] Support splitting middle expressions across EEx clauses
#### Elixir
* [Calendar] Optimize `date_from_iso_days` by using the Neri-Schneider algorithm
* [Enum] Add `Enum.min_max` sorter
* [Integer] Add `Integer.ceil_div/2`
* [IO] Add `IO.iodata_empty?/1`
* [File] Skip device, named pipes, etc in `File.cp_r/3` instead of erroring with reason `:eio`
* [Kernel] Print intermediate results of `dbg` for pipes
* [Kernel] Warn on unused requires
* [Regex] Add `Regex.import/1` to import regexes defined with `/E`
* [Access] Add support for keyword lists in `Access.key/2` and `Access.key!/1`
* [Code] Add support for the `:erlc_options` compiler option
* [Code.Formatter] Add a `:migrate_atom_interpolations` option
* [Kernel] Improve performance of type constructors and complex intersections
* [Kernel] Warn on binary patterns with segments that are not byte-aligned
* [Kernel.ParallelCompiler] Add a hint when spawned processes cannot load modules defined during compilation
* [Keyword] Optimize `Keyword.pop/3`, `Keyword.pop!/2`, and `Keyword.pop_lazy/3`
* [List] Add `List.to_existing_atom/2` and `List.to_unsafe_atom/1`
* [MapSet] Optimize `MapSet.symmetric_difference/2` when set sizes differ
* [Path] Add `Path.safe_join/2`
* [Registry] Optimize exact key matching in lookups
* [String] Optimize `String.bag_distance/2`
* [String] Add `String.to_existing_atom/2` and `String.to_unsafe_atom/1`
* [URI] Optimize percent-decoding and `URI.to_string/1`
#### ExUnit
* [ExUnit.CaptureLog] Add `:formatter` option for custom log formatting
* [ExUnit.Assertions] Add `trace/3` helper
#### Mix
* [mix deps] Support filtering `mix deps` output
* [mix test] Add `mix test --dry-run`
### 2. Hard deprecations
### 2. Bug fixes
#### 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
* [Calendar] Fix `Calendar.strftime/3` formatting of negative years with `%y`
* [Calendar] Fix rounding for `:day`, `:hour`, and `:minute` units in `DateTime.diff/3`, `NaiveDateTime.diff/3`, and `Time.diff/3`
* [Calendar.ISO] Fix `Calendar.ISO.valid_time?/4` to reject non-integer microsecond precision
* [Calendar.ISO] Reject negative zero UTC offsets in basic formats
* [Code.Formatter] Fix rendering calls where `do` is followed by non-block keyword arguments
* [Code.Fragment] Fix cursor completion when operator keywords such as `in`, `when`, `and`, `or`, and `not` follow another operator
* [Date] Preserve the `:format` option in `Date.to_iso8601/2` with custom calendars
* [Date.Range] Fix slicing date ranges with stepped ranges
* [Duration] Reject duplicate seconds in `Duration.from_iso8601/1`
* [Enum] Fix `Enum.min/2,3` and `Enum.max/2,3` with custom sorters on ranges
* [IO.ANSI.Docs] Recognize additional punctuation delimiters when rendering Markdown
* [Kernel] Fix expansion of rebound variables in bitstring size expressions
* [Kernel] Expand `defguard` macros separately in guard and body contexts, preserving `and`/`or` error semantics outside guards
* [Kernel] Fix inferred stacktrace types to allow arbitrary keyword metadata
* [Kernel] Fix inferred types for functions with non-returning clauses
* [Kernel] Fix map field type inference in the presence of empty map types
* [Kernel] Fix tuple fetch and deletion type operations across equivalent tuple types
* [Kernel] Fix variables defined in one default argument leaking into subsequent default arguments
* [Kernel] Improve the error message for non-atom struct keys
* [Kernel] Raise when `|` is used in guards
* [Kernel.Typespec] Preserve metadata when proxying to Elixir typespecs
* [Keyword] Delete duplicate keys when `Keyword.get_and_update/3` and `Keyword.get_and_update!/3` return `:pop`
* [Macro] Properly escape C1 control characters and Unicode noncharacters
* [NaiveDateTime] Fix `NaiveDateTime.diff/3` over-counting incomplete units
* [Range] Fix `Range.disjoint?/2` for ranges beyond floating-point precision
* [Range] Fix `Range.disjoint?/2` for single-element ranges with a negative step
* [String] Fix `String.reverse/1` grapheme ordering around invalid UTF-8 bytes
* [String] Return `1.0` from `String.bag_distance/2` for two empty strings
* [Time] Validate microseconds in `Time.from_seconds_after_midnight/3`
#### Logger
#### ExUnit
* [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`
* [ExUnit.Assertions] Fix `refute_in_delta/4` at the delta boundary and with negative deltas
* [ExUnit.CaptureIO] Stop `StringIO` processes when capturing a named device fails
## v1.19
#### IEx
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).
* [IEx.Autocomplete] Fix completion crashes on maps with non-atom keys
* [IEx.Evaluator] Recognize `**` and `not in` as continuation operators
* [IEx.Helpers] Fix `r/1` when multiple modules are defined in the same file
* [IEx.Helpers] Fix heap and stack memory calculations in `process_info/1`
#### Mix
* [Mix] Prevent synchronization lock files from being overwritten with empty contents
* [Mix.Release] Accept chardata paths in `Mix.Release.make_boot_script/4`
* [Mix.SCM.Git] Raise if Git refspecs start with `-`
* [mix deps] Recompile path and fetchable dependencies when one of the dependencies they were compiled with is removed
* [mix deps] Mark fetchable dependencies for compilation when their build exists but their SCM manifest is missing
* [mix deps.compile] Preserve code paths and compiler options across OS partitions
* [mix format] Pass `:sigils` to plugins invoked for sigils, allowing nested sigils to be formatted
* [mix new] Avoid trailing whitespace in generated files
### 3. Hard deprecations
#### Elixir
* [Macro.Env] `Macro.Env.fetch_alias/2` and `Macro.Env.fetch_macro_alias/2` are deprecated, use `Macro.Env.expand_alias/4` instead
### 4. Soft deprecations
#### Elixir
* [Kernel] Atom interpolation (`:"foo_#{bar}"`) is deprecated in favor of explicit `String.to_unsafe_atom/1`
* [List] `List.to_atom/1` is deprecated in favor of `List.to_unsafe_atom/1`
* [String] `String.to_atom/1` is deprecated in favor of `String.to_unsafe_atom/1`
## v1.20
The CHANGELOG for v1.20 releases can be found [in the v1.20 branch](https://github.com/elixir-lang/elixir/blob/v1.20/CHANGELOG.md).
+86 -47
View File
@@ -55,7 +55,7 @@ To recompile all (including Erlang modules):
make compile
```
After your changes are done, please remember to run `make format` to guarantee
After your changes are done, run `make format` to guarantee
all files are properly formatted, then run the full suite with
`make test`.
@@ -71,53 +71,67 @@ updating an existing checkout, run `make clean compile`. You can check
[the official build status](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml).
More tasks can be found by reading the [Makefile](Makefile).
We encourage contributors to write tests that capture both existing and newly
introduced behavior, especially for bug fixes and major changes:
## Sending a pull request
* **Bug Fixes:** If you are fixing a bug, please try to include a test that
*fails* before your change and *passes* afterward. This makes it easier to
confirm that the fix addresses the underlying issue and helps prevent
regressions in the future.
Contributions are done [via pull request](https://help.github.com/articles/using-pull-requests/)
and must include tests and other relevant proof of work:
* **Bug Fixes:** If you are fixing a bug, include a test that *fails* before
your change and *passes* afterward. This makes it easier to confirm that the
fix addresses the underlying issue and helps prevent regressions in the future.
* **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.
major changes to existing functionality, please add assocaited tests. Aim to
have the best code coverage possible.
With tests running and passing, you are ready to contribute to Elixir and
[send a pull request](https://help.github.com/articles/using-pull-requests/).
We have saved some excellent pull requests we have received in the past in
case you are looking for some examples:
* **Performance improvements:** For performance improvements, please include the
benchmark script, with inputs and results, in the pull request description.
We recommend using [benchee](https://github.com/bencheeorg/benchee).
* [Implement Enum.member? - Pull request](https://github.com/elixir-lang/elixir/pull/992)
Here are some pull requests we have received in the past you can use as reference:
* [Add String.valid? - Pull request](https://github.com/elixir-lang/elixir/pull/1058)
* [Implement Enum.member?](https://github.com/elixir-lang/elixir/pull/992)
* [Implement capture_io for ExUnit - Pull request](https://github.com/elixir-lang/elixir/pull/1059)
* [Add String.valid?](https://github.com/elixir-lang/elixir/pull/1058)
* [Implement capture_io for ExUnit](https://github.com/elixir-lang/elixir/pull/1059)
## Reviewing changes
Once a pull request is sent, the Elixir team will review your changes.
We outline our process below to clarify the roles of everyone involved.
All pull requests must be approved by two committers before being merged into
the repository. If changes are necessary, the team will leave appropriate
comments requesting changes to the code. Unfortunately, we cannot guarantee a
pull request will be merged, even when modifications are requested, as the Elixir
team will re-evaluate the contribution as it changes.
Committers may also push style changes directly to your branch. If you would
rather manage all changes yourself, you can disable the "Allow edits from maintainers"
feature when submitting your pull request.
The Elixir team may optionally assign someone to review a pull request.
If someone is assigned, they must explicitly approve the code before
another team member can merge it.
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.
When the review finishes, your pull request will be squashed and merged
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 cloned and compiled alongside Elixir.
After cloning and compiling Elixir, run:
```sh
elixir_dir=$(pwd)
cd .. && git clone https://github.com/elixir-lang/ex_doc.git
cd ex_doc && "${elixir_dir}/bin/elixir" "${elixir_dir}/bin/mix" do deps.get + compile
# Now we will go back to Elixir's root directory,
cd "${elixir_dir}"
# and generate HTML and EPUB documents:
make docs
```
This will produce documentation sets for `elixir`, `eex`, `ex_unit`, `iex`, `logger`,
and `mix` under the `doc` directory. If you are planning to contribute documentation,
[please check our best practices for writing documentation](https://elixir.hexdocs.pm/writing-documentation.html).
## Licensing and Compliance Requirements
Please review our [Open Source Policy](OPEN_SOURCE_POLICY.md) for complete
@@ -181,25 +195,50 @@ affecting **all external contributors**:
See <https://developercertificate.org/> for a copy of the Developer Certificate
of Origin license.
## Building documentation
## Using AI and coding agents
Building the documentation requires that [ExDoc](https://github.com/elixir-lang/ex_doc)
is installed and built alongside Elixir.
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.
After cloning and compiling Elixir, run:
That said, here are examples of how one might (or might not) use AI and coding
agents in Elixir spaces:
```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
* 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 we will go back to Elixir's root directory,
cd "${elixir_dir}"
* Do not use coding agents to tackle existing issues unless they have the
"Contributions Welcome" label.
# and generate HTML and EPUB documents:
make docs
```
* 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.
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).
* When automating AI usage on the Elixir codebase for performance improvements,
security fixes, or correctness changes to the compiler or type system, pair it
with a separate set of agents whose job is to argue against and try to invalidate
any proposed change. And treat their approval as advisory: a human must still
validate it before opening issues or pull requests.
If any code is written by AI, then you must follow the guidelines below.
### AI contributions
AI agents MUST NOT add Signed-off-by tags. Only humans can legally certify the Developer
Certificate of Origin (DCO). The human submitter is responsible for:
* Reviewing all AI-generated code
* Ensuring compliance with licensing requirements
* Adding their own Signed-off-by tag to certify the DCO
* Taking full responsibility for the contribution
* Disclosing use of AI for comments and code contributions
When AI tools contribute to Elixir, proper attribution helps track the evolving role of
AI in the development process. Contributions should include an Assisted-by tag in the
following format:
Assisted-by: AGENT_NAME:MODEL_VERSION
+3 -3
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 >= 26)])' -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 >= 27)])' -s erlang halt | grep -q '^true'; \
if [ $$? != 0 ]; then \
echo "At least Erlang/OTP 26.0 is required to build Elixir"; \
echo "At least Erlang/OTP 27.0 is required to build Elixir"; \
exit 1; \
fi
endef
@@ -189,7 +189,7 @@ clean_elixir:
#==> Documentation tasks
SOURCE_REF = $(shell tag="$(call GIT_TAG)" revision="$(call GIT_REVISION)"; echo "$${tag:-$$revision}")
DOCS_COMPILE = CANONICAL=$(CANONICAL) bin/elixir ../ex_doc/bin/ex_doc "$(1)" "$(VERSION)" "lib/$(2)/ebin" --main "$(3)" --source-url "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)" --logo lib/elixir/pages/images/logo.png --output doc/$(2) --canonical "https://hexdocs.pm/$(2)/$(CANONICAL)" --homepage-url "https://elixir-lang.org/docs.html" $(DOCS_OPTIONS) $(4)
DOCS_COMPILE = CANONICAL=$(CANONICAL) bin/elixir ../ex_doc/bin/ex_doc "$(1)" "$(VERSION)" "lib/$(2)/ebin" --main "$(3)" --source-url "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)" --logo lib/elixir/pages/images/logo.png --output doc/$(2) --canonical "https://$(2).hexdocs.pm/$(CANONICAL)" --homepage-url "https://elixir-lang.org/docs.html" $(DOCS_OPTIONS) $(4)
DOCS_CONFIG = bin/elixir lib/elixir/scripts/docs_config.exs "$(1)"
docs: compile ../ex_doc/bin/ex_doc docs_elixir docs_eex docs_mix docs_iex docs_ex_unit docs_logger
+6 -5
View File
@@ -7,14 +7,14 @@
<h1>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo-dark.png">
<img alt="Elixir logo" src="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo.png" width="200">
<img alt="Elixir logo" src="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/public/images/logo/logo.png" width="200">
</picture>
</h1>
[![CI](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml?query=branch%3Amain)
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/10187/badge)](https://www.bestpractices.dev/projects/10187)
Elixir is a dynamic, functional language designed for building scalable
Elixir is a programming language designed for building scalable
and maintainable applications.
For more about Elixir, installation and documentation,
@@ -122,9 +122,10 @@ 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. We also include information on our review
process, licensing requirements, and helpful tips to ensure a smooth
contribution experience.
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).
## Development links
+3 -3
View File
@@ -12,12 +12,12 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
Elixir version | Support
:------------- | :-----------------------------
1.20 | Development
1.19 | Bug fixes and security patches
1.21 | Development
1.20 | Bug fixes and security patches
1.19 | Security patches only
1.18 | Security patches only
1.17 | Security patches only
1.16 | Security patches only
1.15 | Security patches only
## Announcements
+1 -1
View File
@@ -1 +1 @@
1.20.0-rc.0
1.21.0-dev
+1 -1
View File
@@ -6,7 +6,7 @@
set -e
ELIXIR_VERSION=1.20.0-rc.0
ELIXIR_VERSION=1.21.0-dev
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.0-rc.0
set ELIXIR_VERSION=1.21.0-dev
if ""%1""=="""" if ""%2""=="""" goto documentation
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
+3 -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(), Code.binding(), [compile_opt]) :: term()
def eval_string(source, bindings \\ [], options \\ [])
when is_binary(source) and is_list(bindings) and is_list(options) do
compiled = compile_string(source, options)
@@ -315,7 +315,7 @@ defmodule EEx do
#=> "foo baz"
"""
@spec eval_file(Path.t(), keyword, [compile_opt]) :: String.t()
@spec eval_file(Path.t(), Code.binding(), [compile_opt]) :: String.t()
def eval_file(filename, bindings \\ [], options \\ [])
when is_list(bindings) and is_list(options) do
filename = IO.chardata_to_string(filename)
@@ -344,6 +344,7 @@ defmodule EEx do
It returns `{:ok, [token]}` where a token is one of:
* `{:comment, content, %{column: column, line: line}}`
* `{:text, content, %{column: column, line: line}}`
* `{:expr, marker, content, %{column: column, line: line}}`
* `{:start_expr, marker, content, %{column: column, line: line}}`
+72 -30
View File
@@ -79,7 +79,7 @@ defmodule EEx.Compiler do
{:error, message <> code_snippet(state.source, state.indentation, meta), meta}
{:ok, expr, new_line, new_column, rest} ->
{key, expr} =
{key, expr, extra_meta} =
case :elixir_tokenizer.tokenize(expr, 1, file: "eex", check_terminators: false) do
{:ok, _line, _column, _warnings, rev_tokens, []} ->
# We ignore warnings because the code will be tokenized
@@ -87,7 +87,7 @@ defmodule EEx.Compiler do
token_key(rev_tokens, expr)
{:error, _, _, _, _} ->
{:expr, expr}
{:expr, expr, %{}}
end
marker =
@@ -102,8 +102,8 @@ defmodule EEx.Compiler do
marker
end
token = {key, marker, expr, %{line: line, column: column}}
trim_and_tokenize(rest, new_line, new_column, state, buffer, acc, &[token | &1])
token = {key, marker, expr, Map.merge(%{line: line, column: column}, extra_meta)}
trim_and_tokenize(rest, new_line, new_column, state, buffer, acc, &merge_token(token, &1))
end
end
@@ -127,6 +127,27 @@ defmodule EEx.Compiler do
tokenize(rest, line, column, state, [{line, column}], fun.(acc))
end
# Merge middle expressions separated only by whitespace so the whitespace is
# part of the Elixir expression, not a separate EEx body.
defp merge_token(
{:middle_expr, ~c"", chars, meta},
[{:text, text, text_meta}, {:middle_expr, ~c"", prev_chars, prev_meta} | acc]
) do
if only_spaces?(text) and clause_block_identifier?(prev_meta) do
[{:middle_expr, ~c"", prev_chars ++ text ++ chars, prev_meta} | acc]
else
[
{:middle_expr, ~c"", chars, meta},
{:text, text, text_meta},
{:middle_expr, ~c"", prev_chars, prev_meta} | acc
]
end
end
defp merge_token(token, acc) do
[token | acc]
end
# Retrieve marker for <%
defp retrieve_marker([marker | t]) when marker in [?=, ?/, ?|] do
@@ -177,35 +198,37 @@ defmodule EEx.Compiler do
defp token_key(rev_tokens, expr) do
case {Enum.reverse(rev_tokens), drop_eol(rev_tokens)} do
{[{:end, _} | _], [{:do, _} | _]} ->
{:middle_expr, expr}
{:middle_expr, expr, %{}}
{_, [{:do, _} | _]} ->
{:start_expr, maybe_append_space(expr)}
{:start_expr, maybe_append_space(expr), %{}}
{_, [{:block_identifier, _, _} | _]} ->
{:middle_expr, maybe_append_space(expr)}
{_, [{:block_identifier, _, identifier} | _]} ->
{:middle_expr, maybe_append_space(expr), %{block_identifier: identifier}}
{[{:end, _} | _], [{:stab_op, _, _} | _]} ->
{:middle_expr, expr}
{:middle_expr, expr, %{}}
{_, [{:stab_op, _, _} | reverse_tokens]} ->
fn_index = Enum.find_index(reverse_tokens, &match?({:fn, _}, &1)) || :infinity
end_index = Enum.find_index(reverse_tokens, &match?({:end, _}, &1)) || :infinity
if end_index > fn_index do
{:start_expr, expr}
{_, [{:stab_op, _, _} | rev_tokens]} ->
if fn_before_end?(rev_tokens) do
{:start_expr, expr, %{}}
else
{:middle_expr, expr}
{:middle_expr, expr, %{}}
end
{tokens, _} ->
case Enum.drop_while(tokens, &closing_bracket?/1) do
[{:end, _} | _] -> {:end_expr, expr}
_ -> {:expr, expr}
[{:end, _} | _] -> {:end_expr, expr, %{}}
_ -> {:expr, expr, %{}}
end
end
end
defp fn_before_end?([{:fn, _} | _]), do: true
defp fn_before_end?([{:end, _} | _]), do: false
defp fn_before_end?([_ | rev_tokens]), do: fn_before_end?(rev_tokens)
defp fn_before_end?([]), do: false
defp drop_eol([{:eol, _} | rest]), do: drop_eol(rest)
defp drop_eol(rest), do: rest
@@ -303,7 +326,7 @@ defmodule EEx.Compiler do
file: file,
source: source,
line: line,
quoted: [],
quoted: %{},
parser_options: [indentation: indentation] ++ parser_options,
indentation: indentation
}
@@ -347,7 +370,7 @@ defmodule EEx.Compiler do
state.parser_options
expr = Code.string_to_quoted!(chars, options)
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), expr)
buffer = handle_expr(buffer, mark, expr, meta, state)
generate_buffer(rest, buffer, scope, state)
end
@@ -366,7 +389,7 @@ 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
@@ -376,7 +399,7 @@ defmodule EEx.Compiler do
IO.warn(message, file: state.file, line: meta.line, column: meta.column)
end
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), contents)
buffer = handle_expr(buffer, mark, contents, meta, state)
generate_buffer(rest, buffer, scope, state)
end
@@ -410,7 +433,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!(wrapped, options)
tuples = Code.string_to_quoted!(:lists.flatten(wrapped), options)
buffer = insert_quoted(tuples, state.quoted)
{buffer, rest}
end
@@ -426,7 +449,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(content, line, column, state)
expr_meta = non_whitespace_meta(:lists.flatten(content), line, column, state)
syntax_error!(message, expr_meta, state)
end
@@ -443,10 +466,10 @@ defmodule EEx.Compiler do
defp wrap_expr(current, line, buffer, chars, state) do
new_lines = List.duplicate(?\n, line - state.line)
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]}
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))}
{count, new_state}
end
@@ -476,11 +499,16 @@ defmodule EEx.Compiler do
Enum.all?(chars, &(&1 in @all_spaces))
end
defp clause_block_identifier?(%{block_identifier: identifier}) do
identifier in [:else, :rescue, :catch]
end
defp clause_block_identifier?(_meta), do: false
# Changes placeholder to real expression
defp insert_quoted({:__EEX__, _, [key]}, quoted) do
{^key, value} = List.keyfind(quoted, key, 0)
value
Map.fetch!(quoted, key)
end
defp insert_quoted({left, line, right}, quoted) do
@@ -513,6 +541,20 @@ 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
+1 -1
View File
@@ -195,7 +195,7 @@ defmodule EEx.Engine do
def handle_expr(state, "=", ast) do
check_state!(state)
%{binary: binary, dynamic: dynamic, vars_count: vars_count} = state
var = Macro.var(:"arg#{vars_count}", __MODULE__)
var = Macro.var(String.to_unsafe_atom("arg#{vars_count}"), __MODULE__)
ast =
quote do
+2 -2
View File
@@ -270,7 +270,7 @@ defmodule EEx.TokenizerTest do
{:text, ~c"foo ", %{column: 1, line: 1}},
{:start_expr, ~c"", ~c" if true do ", %{column: 5, line: 1}},
{:text, ~c"bar", %{column: 21, line: 1}},
{:middle_expr, ~c"", ~c" else ", %{column: 24, line: 1}},
{:middle_expr, ~c"", ~c" else ", %{block_identifier: :else, column: 24, line: 1}},
{:text, ~c"baz", %{column: 34, line: 1}},
{:end_expr, ~c"", ~c" end ", %{column: 37, line: 1}},
{:eof, %{column: 46, line: 1}}
@@ -286,7 +286,7 @@ defmodule EEx.TokenizerTest do
exprs = [
{:start_expr, ~c"=", ~c" if true do ", %{column: 2, line: 1}},
{:text, ~c"\n TRUE \n", %{column: 20, line: 1}},
{:middle_expr, ~c"", ~c" else ", %{column: 3, line: 3}},
{:middle_expr, ~c"", ~c" else ", %{block_identifier: :else, column: 3, line: 3}},
{:text, ~c"\n FALSE \n", %{column: 13, line: 3}},
{:end_expr, ~c"", ~c" end ", %{column: 3, line: 5}},
{:eof, %{column: 3, line: 7}}
+44 -1
View File
@@ -262,6 +262,37 @@ defmodule EExTest do
assert_eval("foo 1,2,3", "foo <% require Enum, as: E %><%= E.join [1, 2, 3], \",\" %>")
end
test "with expression with else clause split across tags" do
template = """
<%= with {:ok, x} <- @res do %>
<p><%= x %></p>
<% else %>
<% _ -> %>
<p>bad</p>
<% end %>
"""
assert_eval("\n <p>ok</p>\n\n", template, [assigns: [res: {:ok, "ok"}]],
engine: EEx.SmartEngine
)
assert_eval("\n <p>bad</p>\n\n", template, [assigns: [res: :error]],
engine: EEx.SmartEngine
)
end
test "empty clauses separated by whitespace" do
template = """
<%= case x do %>
<% :foo -> %>
<% :bar -> %>
<% end %>
"""
assert_eval("\n \n", template, x: :foo)
assert_eval("\n\n", template, x: :bar)
end
test "with end of token" do
assert_eval("foo bar %>", "foo bar %>")
end
@@ -522,7 +553,7 @@ defmodule EExTest do
assert message |> Exception.message() |> strip_ansi() =~ """
│
514 │ true && @some[\s
#{line + 2} │ true && @some[\s
│ │ └ missing closing delimiter (expected "]")
│ └ unclosed delimiter
"""
@@ -575,6 +606,18 @@ 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
+10 -5
View File
@@ -8,8 +8,13 @@
Code.require_file("../../elixir/scripts/cover_record.exs", __DIR__)
CoverageRecorder.maybe_record("eex")
ExUnit.start(
trace: !!System.get_env("TRACE"),
include: line_include,
exclude: line_exclude
)
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)
+90 -29
View File
@@ -10,8 +10,8 @@ defmodule Access do
keys of any type in a data structure via the `data[key]` syntax.
`Access` supports keyword lists (`Keyword`) and maps (`Map`) out
of the box. Keywords supports only atoms keys, keys for maps can
be of any type. Both return `nil` if the key does not exist:
of the box. Keyword lists support only atom keys, while keys for maps
can be of any type. Both return `nil` if the key does not exist:
iex> keywords = [a: 1, b: 2]
iex> keywords[:a]
@@ -33,7 +33,7 @@ defmodule Access do
iex> keywords[:c][:unknown]
nil
This works because accessing anything on a `nil` value, returns
This works because accessing anything on a `nil` value returns
`nil` itself:
iex> nil[:a]
@@ -226,6 +226,8 @@ defmodule Access do
end
end
defguardp is_probably_keyword(list) when list == [] or is_atom(elem(hd(list), 0))
@doc """
Fetches the value for the given key in a container (a map, keyword
list, or struct that implements the `Access` behaviour).
@@ -485,7 +487,7 @@ defmodule Access do
## Accessors
@doc """
Returns a function that accesses the given key in a map/struct.
Returns a function that accesses the given key in a map/struct/keyword list.
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
`Kernel.get_and_update_in/3`, and friends.
@@ -514,30 +516,56 @@ defmodule Access do
iex> pop_in(map, [Access.key(:user), Access.key(:name)])
{"john", %{user: %{}}}
An error is raised if the accessed structure is not a map or a struct:
iex> keyword = [user: [name: "john"]]
iex> get_in(keyword, [Access.key(:unknown, []), Access.key(:name, "john")])
"john"
iex> get_and_update_in(keyword, [Access.key(:user), Access.key(:name)], fn prev ->
...> {prev, String.upcase(prev)}
...> end)
{"john", [user: [name: "JOHN"]]}
iex> pop_in(keyword, [Access.key(:user), Access.key(:name)])
{"john", [user: []]}
iex> get_in([], [Access.key(:foo)])
** (BadMapError) expected a map, got:
...
An error is raised if the accessed structure is not a map, struct, or keyword list:
iex> get_in(123, [Access.key(:foo)])
** (RuntimeError) Access.key/2 expected a map/struct/keyword list, got: ...
iex> put_in([1, 2, 3], [Access.key(:foo)], :bar)
** (RuntimeError) Access.key/2 expected a map/struct/keyword list, got: ...
"""
@spec key(key, term) :: access_fun(data :: struct | map, current_value :: term)
@spec key(key, term) :: access_fun(data :: struct | map | keyword, current_value :: term)
def key(key, default \\ nil) do
fn
:get, data, next ->
:get, %{} = data, next ->
next.(Map.get(data, key, default))
:get_and_update, data, next ->
:get_and_update, %{} = data, next ->
value = Map.get(data, key, default)
case next.(value) do
{get, update} -> {get, Map.put(data, key, update)}
:pop -> {value, Map.delete(data, key)}
end
:get, data, next when is_probably_keyword(data) ->
next.(Keyword.get(data, key, default))
:get_and_update, data, next when is_probably_keyword(data) ->
value = Keyword.get(data, key, default)
case next.(value) do
{get, update} -> {get, Keyword.put(data, key, update)}
:pop -> {value, Keyword.delete(data, key)}
end
_op, data, _next ->
raise "Access.key/2 expected a map/struct/keyword list, got: #{inspect(data)}"
end
end
@doc """
Returns a function that accesses the given key in a map/struct.
Returns a function that accesses the given key in a map/struct/keyword list.
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
`Kernel.get_and_update_in/3`, and friends.
@@ -546,6 +574,19 @@ defmodule Access do
## Examples
iex> keyword = [user: [name: "john"]]
iex> get_in(keyword, [Access.key!(:user), Access.key!(:name)])
"john"
iex> get_and_update_in(keyword, [Access.key!(:user), Access.key!(:name)], fn prev ->
...> {prev, String.upcase(prev)}
...> end)
{"john", [user: [name: "JOHN"]]}
iex> pop_in(keyword, [Access.key!(:user), Access.key!(:name)])
{"john", [user: []]}
iex> get_in(keyword, [Access.key!(:user), Access.key!(:unknown)])
** (KeyError) key :unknown not found in:
...
iex> map = %{user: %{name: "john"}}
iex> get_in(map, [Access.key!(:user), Access.key!(:name)])
"john"
@@ -574,13 +615,15 @@ defmodule Access do
`Access.key!/1` is useful when the key is not known in advance
and must be accessed dynamically.
An error is raised if the accessed structure is not a map/struct:
An error is raised if the accessed structure is not a map/struct/keyword list:
iex> get_in([], [Access.key!(:foo)])
** (RuntimeError) Access.key!/1 expected a map/struct, got: []
iex> get_in(123, [Access.key!(:foo)])
** (RuntimeError) Access.key!/1 expected a map/struct/keyword list, got: 123
iex> put_in([1, 2, 3], [Access.key!(:foo)], :bar)
** (RuntimeError) Access.key!/1 expected a map/struct/keyword list, got: ...
"""
@spec key!(key) :: access_fun(data :: struct | map, current_value :: term)
@spec key!(key) :: access_fun(data :: struct | map | keyword, current_value :: term)
def key!(key) do
fn
:get, %{} = data, next ->
@@ -594,8 +637,19 @@ defmodule Access do
:pop -> {value, Map.delete(data, key)}
end
:get, data, next when is_probably_keyword(data) ->
next.(Keyword.fetch!(data, key))
:get_and_update, data, next when is_probably_keyword(data) ->
value = Keyword.fetch!(data, key)
case next.(value) do
{get, update} -> {get, Keyword.put(data, key, update)}
:pop -> {value, Keyword.delete(data, key)}
end
_op, data, _next ->
raise "Access.key!/1 expected a map/struct, got: #{inspect(data)}"
raise "Access.key!/1 expected a map/struct/keyword list, got: #{inspect(data)}"
end
end
@@ -863,7 +917,7 @@ defmodule Access do
iex> pop_in(list, [Access.filter(&(&1.salary >= 20)), :name])
{["francine"], [%{name: "john", salary: 10}, %{salary: 30}]}
When no match is found, an empty list is returned and the update function is never called
When no match is found, an empty list is returned and the update function is never called:
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
iex> get_in(list, [Access.filter(&(&1.salary >= 50)), :name])
@@ -880,7 +934,7 @@ defmodule Access do
"""
@doc since: "1.6.0"
@spec filter((term -> boolean)) :: access_fun(data :: list, current_value :: list)
@spec filter((term -> as_boolean(term))) :: access_fun(data :: list, current_value :: list)
def filter(func) when is_function(func) do
fn op, data, next -> filter(op, data, func, next) end
end
@@ -981,12 +1035,12 @@ defmodule Access do
end
defp slice(:get_and_update, data, range, next) when is_list(data) do
range = normalize_range(range, data)
%Range{first: first, last: last, step: step} = normalize_range(range, data)
if range.first > range.last do
if first > last do
{[], data}
else
get_and_update_slice(data, range, next, [], [], 0)
get_and_update_slice(data, first, last, step, next, [], [], 0)
end
end
@@ -1091,16 +1145,23 @@ defmodule Access do
defp normalize_range(range, _list), do: range
defp get_and_update_slice([head | rest], range, next, updates, gets, index) do
if index in range do
defp get_and_update_slice(rest, _first, last, _step, _next, updates, gets, index)
when index > last do
{:lists.reverse(gets), :lists.reverse(updates, rest)}
end
defp get_and_update_slice([head | rest], first, last, step, next, updates, gets, index) do
if index >= first and rem(index - first, step) == 0 do
case next.(head) do
:pop ->
get_and_update_slice(rest, range, next, updates, [head | gets], index + 1)
get_and_update_slice(rest, first, last, step, next, updates, [head | gets], index + 1)
{get, update} ->
get_and_update_slice(
rest,
range,
first,
last,
step,
next,
[update | updates],
[get | gets],
@@ -1108,11 +1169,11 @@ defmodule Access do
)
end
else
get_and_update_slice(rest, range, next, [head | updates], gets, index + 1)
get_and_update_slice(rest, first, last, step, next, [head | updates], gets, index + 1)
end
end
defp get_and_update_slice([], _range, _next, updates, gets, _index) do
defp get_and_update_slice([], _first, _last, _step, _next, updates, gets, _index) do
{:lists.reverse(gets), :lists.reverse(updates)}
end
@@ -1139,7 +1200,7 @@ defmodule Access do
iex> pop_in(list, [Access.find(&(&1.salary <= 40))])
{%{name: "john", salary: 10}, [%{name: "francine", salary: 30}]}
When no match is found, nil is returned and the update function is never called
When no match is found, nil is returned and the update function is never called:
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
iex> get_in(list, [Access.find(&(&1.salary >= 50)), :name])
+3 -3
View File
@@ -327,7 +327,7 @@ defmodule Agent do
passing the agent state. The result of the function invocation is
returned from this function.
`timeout` is an integer greater than zero which specifies how many
`timeout` is a non-negative integer which specifies how many
milliseconds are allowed before the agent executes the function and returns
the result value, or the atom `:infinity` to wait indefinitely. If no result
is received within the specified time, the function call fails and the caller
@@ -366,7 +366,7 @@ defmodule Agent do
elements, the first being the value to return (that is, the "get" value)
and the second one being the new state of the agent.
`timeout` is an integer greater than zero which specifies how many
`timeout` is a non-negative integer which specifies how many
milliseconds are allowed before the agent executes the function and returns
the result value, or the atom `:infinity` to wait indefinitely. If no result
is received within the specified time, the function call fails and the caller
@@ -407,7 +407,7 @@ defmodule Agent do
This function always returns `:ok`.
`timeout` is an integer greater than zero which specifies how many
`timeout` is a non-negative integer which specifies how many
milliseconds are allowed before the agent executes the function and returns
the result value, or the atom `:infinity` to wait indefinitely. If no result
is received within the specified time, the function call fails and the caller
+10 -5
View File
@@ -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, we will use
this as a point of illustration.
Since Mix configurations are commonly used to configure applications (including
your dependencies), 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`.
environment of `:my_app`. For example, your `config/runtime.exs` file might have:
config :my_app, Databases.RepoOne,
# A database configuration
@@ -714,6 +714,11 @@ 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
@@ -991,7 +996,7 @@ defmodule Application do
end
@doc """
Gets the directory for app.
Gets the directory for `app`.
This information is returned based on the code path. Here is an
example:
@@ -1072,7 +1077,7 @@ defmodule Application do
@doc """
Formats the error reason returned by `start/2`,
`ensure_started/2`, `stop/1`, `load/1` and `unload/1`,
returns a string.
and returns a string.
"""
@spec format_error(any) :: String.t()
def format_error(reason) do
+294 -70
View File
@@ -155,6 +155,255 @@ 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.
@@ -187,7 +436,7 @@ defmodule Base do
end
for {base, alphabet} <- [upper: b16_alphabet, lower: to_lower_enc.(b16_alphabet)] do
name = :"encode16#{base}"
name = String.to_unsafe_atom("encode16#{base}")
encoded = to_encode_list.(alphabet)
@compile {:inline, [{name, 1}]}
@@ -287,7 +536,7 @@ defmodule Base do
* `:lower` - only allows lower case characters
* `:mixed` - allows mixed case characters
An `ArgumentError` exception is raised if the padding is incorrect or
An `ArgumentError` exception is raised if the string has an odd length or
a non-alphabet character is present in the string.
## Examples
@@ -368,28 +617,24 @@ defmodule Base do
upper = Enum.with_index(b16_alphabet)
for {base, alphabet} <- [upper: upper, lower: to_lower_dec.(upper), mixed: to_mixed_dec.(upper)] do
decode_name = :"decode16#{base}!"
validate_name = :"validate16#{base}?"
valid_char_name = :"valid_char16#{base}?"
decode_name = String.to_unsafe_atom("decode16#{base}!")
validate_name = String.to_unsafe_atom("validate16#{base}?")
valid_char_name = String.to_unsafe_atom("valid_char16#{base}?")
valid_word_name = String.to_unsafe_atom("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)(<<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(validate_name)(<<char, rest::binary>>),
do: unquote(valid_char_name)(char) and unquote(validate_name)(rest)
defp unquote(decode_name)(char) do
index = char - unquote(min)
@@ -490,7 +735,7 @@ defmodule Base do
end
for {base, alphabet} <- [base: b64_alphabet, url: b64url_alphabet] do
name = :"encode64#{base}"
name = String.to_unsafe_atom("encode64#{base}")
encoded = to_encode_list.(alphabet)
@compile {:inline, [{name, 1}]}
@@ -756,28 +1001,24 @@ defmodule Base do
end
for {base, alphabet} <- [base: b64_alphabet, url: b64url_alphabet] do
decode_name = :"decode64#{base}!"
decode_name = String.to_unsafe_atom("decode64#{base}!")
validate_name = :"validate64#{base}?"
validate_main_name = :"validate_main64#{validate_name}?"
valid_char_name = :"valid_char64#{base}?"
validate_name = String.to_unsafe_atom("validate64#{base}?")
validate_main_name = String.to_unsafe_atom("validate_main64#{validate_name}?")
valid_char_name = String.to_unsafe_atom("valid_char64#{base}?")
valid_word_name = String.to_unsafe_atom("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)(
<<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_main_name)(<<char, rest::binary>>),
do: unquote(valid_char_name)(char) and unquote(validate_main_name)(rest)
defp unquote(validate_name)(<<>>, _pad?), do: true
@@ -863,13 +1104,6 @@ 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)
@@ -1080,7 +1314,7 @@ defmodule Base do
hexupper: b32hex_alphabet,
hexlower: to_lower_enc.(b32hex_alphabet)
] do
name = :"encode32#{base}"
name = String.to_unsafe_atom("encode32#{base}")
encoded = to_encode_list.(alphabet)
@compile {:inline, [{name, 1}]}
@@ -1154,7 +1388,7 @@ defmodule Base do
The values for `:case` can be:
* `:upper` - only allows upper case characters (default)
* `:upper` - only allows upper case characters (default)
* `:lower` - only allows lower case characters
* `:mixed` - allows mixed case characters
@@ -1419,27 +1653,24 @@ defmodule Base do
hexlower: to_lower_dec.(hexupper),
hexmixed: to_mixed_dec.(hexupper)
] do
decode_name = :"decode32#{base}!"
validate_name = :"validate32#{base}?"
validate_main_name = :"validate_main32#{validate_name}?"
valid_char_name = :"valid_char32#{base}?"
decode_name = String.to_unsafe_atom("decode32#{base}!")
validate_name = String.to_unsafe_atom("validate32#{base}?")
validate_main_name = String.to_unsafe_atom("validate_main32#{validate_name}?")
valid_char_name = String.to_unsafe_atom("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 = String.to_unsafe_atom("valid_word32#{base}?")
defp unquote(validate_main_name)(<<w::56, rest::binary>>),
do: unquote(valid_word_name)(w) and unquote(validate_main_name)(rest)
defp unquote(validate_main_name)(<<>>), do: true
defp unquote(validate_main_name)(
<<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_main_name)(<<char, rest::binary>>),
do: unquote(valid_char_name)(char) and unquote(validate_main_name)(rest)
defp unquote(validate_name)(<<>>, _pad?), do: true
@@ -1519,13 +1750,6 @@ 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)
+56 -29
View File
@@ -3,6 +3,8 @@
# 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.
@@ -104,6 +106,7 @@ defmodule Calendar do
@typedoc "Any map or struct that contains the time fields."
@type time :: %{
optional(any) => any,
calendar: calendar,
hour: hour,
minute: minute,
second: second,
@@ -293,7 +296,7 @@ defmodule Calendar do
@callback time_from_day_fraction(day_fraction) :: {hour, minute, second, microsecond}
@doc """
Define the rollover moment for the calendar.
Defines the rollover moment for the calendar.
This is the moment, in your calendar, when the current day ends
and the next day starts.
@@ -379,13 +382,13 @@ defmodule Calendar do
@callback iso_days_to_end_of_day(iso_days) :: iso_days
@doc """
Shifts date by given duration according to its calendar.
Shifts date by the given duration according to its calendar.
"""
@doc since: "1.17.0"
@callback shift_date(year, month, day, Duration.t()) :: {year, month, day}
@doc """
Shifts naive datetime by given duration according to its calendar.
Shifts naive datetime by the given duration according to its calendar.
"""
@doc since: "1.17.0"
@callback shift_naive_datetime(
@@ -400,7 +403,7 @@ defmodule Calendar do
) :: {year, month, day, hour, minute, second, microsecond}
@doc """
Shifts time by given duration according to its calendar.
Shifts time by the given duration according to its calendar.
"""
@doc since: "1.17.0"
@callback shift_time(hour, minute, second, microsecond, Duration.t()) ::
@@ -529,6 +532,7 @@ 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
@@ -559,11 +563,11 @@ defmodule Calendar do
P | "am" or "pm" (noon is "pm", midnight as "am") | am, pm
q | Quarter | 1, 2, 3, 4
s | Number of seconds since the Epoch, 1970-01-01 00:00:00+0000 (UTC) | 1565888877
S | Second | 00, 59, 60
S | Second | 00, 59
u | Day of the week | 1 (Monday), 7 (Sunday)
x | Preferred date (without time) representation | 2018-10-17
X | Preferred time (without date) representation | 12:34:56
y | Year as 2-digits | 01, 01, 86, 18
y | Year as 2-digits | -01, 01, 86, 18
Y | Year | -0001, 0001, 1986
z | +hhmm/-hhmm time zone offset from UTC (empty string if naive) | +0300, -0530
Z | Time zone abbreviation (empty string if naive) | CET, BRST
@@ -620,7 +624,7 @@ defmodule Calendar do
...>)
"серпень"
Microsecond formatting:
Microsecond formatting:
iex> Calendar.strftime(~U[2019-08-26 13:52:06Z], "%y-%m-%d %H:%M:%S.%f")
"19-08-26 13:52:06.0"
@@ -667,9 +671,13 @@ defmodule Calendar do
end
defp parse_modifiers(<<digit, rest::binary>>, width, pad, parser_data) when digit in ?0..?9 do
new_width = (width || 0) * 10 + (digit - ?0)
width = (width || 0) * 10 + (digit - ?0)
parse_modifiers(rest, new_width, pad, parser_data)
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)
end
# set default padding if none was specified
@@ -704,7 +712,7 @@ defmodule Calendar do
# Literally just %
defp format_modifiers("%" <> rest, width, pad, datetime, format_options, acc) do
parse(rest, datetime, format_options, [pad_leading("%", width, pad) | acc])
parse(rest, datetime, format_options, [pad_leading_ascii("%", width, pad) | acc])
end
# Abbreviated name of day
@@ -773,7 +781,7 @@ defmodule Calendar do
# Day of the month
defp format_modifiers("d" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.day |> Integer.to_string() |> pad_leading(width, pad)
result = datetime.day |> Integer.to_string() |> pad_leading_ascii(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
@@ -792,31 +800,35 @@ defmodule Calendar do
# Hour using a 24-hour clock
defp format_modifiers("H" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.hour |> Integer.to_string() |> pad_leading(width, pad)
result = datetime.hour |> Integer.to_string() |> pad_leading_ascii(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Hour using a 12-hour clock
defp format_modifiers("I" <> rest, width, pad, datetime, format_options, acc) do
result = (rem(datetime.hour + 23, 12) + 1) |> Integer.to_string() |> pad_leading(width, pad)
result =
(rem(datetime.hour + 23, 12) + 1) |> Integer.to_string() |> pad_leading_ascii(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Day of the year
defp format_modifiers("j" <> rest, width, pad, datetime, format_options, acc) do
result = datetime |> Date.day_of_year() |> Integer.to_string() |> pad_leading(width, pad)
result =
datetime |> Date.day_of_year() |> Integer.to_string() |> pad_leading_ascii(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Month
defp format_modifiers("m" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.month |> Integer.to_string() |> pad_leading(width, pad)
result = datetime.month |> Integer.to_string() |> pad_leading_ascii(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Minute
defp format_modifiers("M" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.minute |> Integer.to_string() |> pad_leading(width, pad)
result = datetime.minute |> Integer.to_string() |> pad_leading_ascii(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
@@ -844,19 +856,23 @@ defmodule Calendar do
# Quarter
defp format_modifiers("q" <> rest, width, pad, datetime, format_options, acc) do
result = datetime |> Date.quarter_of_year() |> Integer.to_string() |> pad_leading(width, pad)
result =
datetime |> Date.quarter_of_year() |> Integer.to_string() |> pad_leading_ascii(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Second
defp format_modifiers("S" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.second |> Integer.to_string() |> pad_leading(width, pad)
result = datetime.second |> Integer.to_string() |> pad_leading_ascii(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Day of the week
defp format_modifiers("u" <> rest, width, pad, datetime, format_options, acc) do
result = datetime |> Date.day_of_week() |> Integer.to_string() |> pad_leading(width, pad)
result =
datetime |> Date.day_of_week() |> Integer.to_string() |> pad_leading_ascii(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
@@ -906,20 +922,25 @@ defmodule Calendar do
# Year as 2-digits
defp format_modifiers("y" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.year |> rem(100) |> Integer.to_string() |> pad_leading(width, pad)
result =
if datetime.year < 0 do
[?- | -datetime.year |> rem(100) |> Integer.to_string() |> pad_leading_ascii(width, pad)]
else
datetime.year |> rem(100) |> Integer.to_string() |> pad_leading_ascii(width, pad)
end
parse(rest, datetime, format_options, [result | acc])
end
# Year
defp format_modifiers("Y" <> rest, width, pad, datetime, format_options, acc) do
{sign, year} =
result =
if datetime.year < 0 do
{?-, -datetime.year}
[?- | -datetime.year |> Integer.to_string() |> pad_leading_ascii(width, pad)]
else
{[], datetime.year}
datetime.year |> Integer.to_string() |> pad_leading_ascii(width, pad)
end
result = [sign | year |> Integer.to_string() |> pad_leading(width, pad)]
parse(rest, datetime, format_options, [result | acc])
end
@@ -966,7 +987,7 @@ defmodule Calendar do
Integer.to_string(div(absolute_offset, 3600) * 100 + rem(div(absolute_offset, 60), 60))
sign = if utc_offset + std_offset >= 0, do: "+", else: "-"
result = "#{sign}#{pad_leading(offset_number, width, pad)}"
result = "#{sign}#{pad_leading_ascii(offset_number, width, pad)}"
parse(rest, datetime, format_options, [result | acc])
end
@@ -985,13 +1006,19 @@ defmodule Calendar do
raise ArgumentError, "invalid strftime format: %#{next}"
end
defp pad_preferred(result, width, pad) when length(result) < width do
pad_preferred([pad | result], width, pad)
defp pad_preferred(result, width, pad) do
result
|> IO.iodata_to_binary()
|> pad_leading(width, pad)
end
defp pad_preferred(result, _width, _pad), do: result
defp pad_leading(string, count, padding) do
to_pad = count - String.length(string)
if to_pad > 0, do: do_pad_leading(to_pad, padding, string), else: string
end
# Similar to `pad_leading/3`, but only for strings that always ASCII-only
defp pad_leading_ascii(string, count, padding) do
to_pad = count - byte_size(string)
if to_pad > 0, do: do_pad_leading(to_pad, padding, string), else: string
end
+16 -12
View File
@@ -81,7 +81,7 @@ defmodule Date do
Ranges of dates can be increasing (`first <= last`) and are
always inclusive. For a decreasing range, use `range/3` with
a step of -1 as first argument.
a step of -1 as third argument.
## Examples
@@ -160,7 +160,7 @@ defmodule Date do
) do
raise ArgumentError,
"both dates must have matching calendar and the step must be a " <>
"non-zero integer, got: #{inspect(first)}, #{inspect(last)}, #{step}"
"non-zero integer, got: #{inspect(first)}, #{inspect(last)}, #{inspect(step)}"
end
defp range(first, first_days, last, last_days, calendar, step) do
@@ -422,7 +422,7 @@ defmodule Date do
def to_iso8601(%{calendar: _} = date, format) when format in [:basic, :extended] do
date
|> convert!(Calendar.ISO)
|> to_iso8601()
|> to_iso8601(format)
end
@doc """
@@ -556,14 +556,18 @@ defmodule Date do
"""
@doc since: "1.4.0"
@spec compare(Calendar.date(), Calendar.date()) :: :lt | :eq | :gt
def compare(%{calendar: calendar} = date1, %{calendar: calendar} = date2) do
%{year: year1, month: month1, day: day1} = date1
%{year: year2, month: month2, day: day2} = date2
case {{year1, month1, day1}, {year2, month2, day2}} do
{first, second} when first > second -> :gt
{first, second} when first < second -> :lt
_ -> :eq
def compare(
%{year: year1, month: month1, day: day1, calendar: calendar},
%{year: year2, month: month2, day: day2, calendar: calendar}
) do
cond do
year1 > year2 -> :gt
year1 < year2 -> :lt
month1 > month2 -> :gt
month1 < month2 -> :lt
day1 > day2 -> :gt
day1 < day2 -> :lt
true -> :eq
end
end
@@ -1047,7 +1051,7 @@ defmodule Date do
@doc """
Calculates the quarter of the year of a given `date`.
Returns the day of the year as an integer. For the ISO 8601
Returns the quarter of the year as an integer. For the ISO 8601
calendar (the default), it is an integer from 1 to 4.
## Examples
+26 -46
View File
@@ -37,21 +37,19 @@ defmodule Date.Range do
first_in_iso_days: first_days,
last_in_iso_days: last_days,
step: step
} = range,
},
%Date{calendar: calendar} = date
) do
{days, _} = Date.to_iso_days(date)
cond do
empty?(range) ->
{:ok, false}
in_range? =
if step > 0 do
first_days <= days and days <= last_days and rem(days - first_days, step) == 0
else
last_days <= days and days <= first_days and rem(days - first_days, step) == 0
end
first_days <= last_days ->
{:ok, first_days <= days and days <= last_days and rem(days - first_days, step) == 0}
true ->
{:ok, last_days <= days and days <= first_days and rem(days - first_days, step) == 0}
end
{:ok, in_range?}
end
def member?(%Date.Range{step: _}, _) do
@@ -59,11 +57,20 @@ defmodule Date.Range do
end
# TODO: Remove me on v2.0
def member?(
%{__struct__: Date.Range, first_in_iso_days: first_days, last_in_iso_days: last_days} =
date_range,
date
) do
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
step = if first_days <= last_days, do: 1, else: -1
member?(Map.put(date_range, :step, step), date)
end
@@ -79,7 +86,7 @@ defmodule Date.Range do
step: step
} = range
) do
{:ok, size(range), &slice(first + &1 * step, step + &3 - 1, &2, calendar)}
{:ok, size(range), &slice(first + &1 * step, step * &3, &2, calendar)}
end
# TODO: Remove me on v2.0
@@ -176,7 +183,7 @@ defmodule Date.Range do
last_in_iso_days: last_days,
step: step
}),
do: abs(div(last_days - first_days, step)) + 1
do: div(last_days - first_days, step) + 1
# TODO: Remove me on v2.0
defp size(
@@ -186,43 +193,16 @@ defmodule Date.Range do
step = if first_days <= last_days, do: 1, else: -1
size(Map.put(date_range, :step, step))
end
defp empty?(%Date.Range{
first_in_iso_days: first_days,
last_in_iso_days: last_days,
step: step
})
when step > 0 and first_days > last_days,
do: true
defp empty?(%Date.Range{
first_in_iso_days: first_days,
last_in_iso_days: last_days,
step: step
})
when step < 0 and first_days < last_days,
do: true
defp empty?(%Date.Range{step: _}), do: false
# TODO: Remove me on v2.0
defp empty?(
%{__struct__: Date.Range, first_in_iso_days: first_days, last_in_iso_days: last_days} =
date_range
) do
step = if first_days <= last_days, do: 1, else: -1
empty?(Map.put(date_range, :step, step))
end
end
defimpl Inspect do
import Kernel, except: [inspect: 2]
def inspect(%Date.Range{first: first, last: last, step: 1}, _) do
def inspect(%Date.Range{first: first, last: last, step: 1}, %Inspect.Opts{}) do
"Date.range(" <> inspect(first) <> ", " <> inspect(last) <> ")"
end
def inspect(%Date.Range{first: first, last: last, step: step}, _) do
def inspect(%Date.Range{first: first, last: last, step: step}, %Inspect.Opts{}) do
"Date.range(" <> inspect(first) <> ", " <> inspect(last) <> ", #{step})"
end
+41 -47
View File
@@ -180,7 +180,7 @@ defmodule DateTime do
since v1.15.0.
The default unit if none gets passed is `:native`,
which results on a default resolution of microseconds.
which results in a default resolution of microseconds.
## Examples
@@ -720,6 +720,9 @@ defmodule DateTime do
Other time zone databases can be passed as argument or set globally.
See the "Time zone database" section in the module docs.
Shifting to the `"Etc/UTC"` time zone always succeeds without
consulting the `time_zone_database`.
## Examples
iex> {:ok, pacific_datetime} = DateTime.shift_zone(~U[2018-07-16 10:00:00Z], "America/Los_Angeles", FakeTimeZoneDatabase)
@@ -753,6 +756,28 @@ defmodule DateTime do
|> shift_zone_for_iso_days_utc(calendar, precision, time_zone, time_zone_database)
end
defp shift_zone_for_iso_days_utc(iso_days_utc, calendar, precision, "Etc/UTC", _time_zone_db) do
{year, month, day, hour, minute, second, {microsecond, _}} =
calendar.naive_datetime_from_iso_days(iso_days_utc)
datetime = %DateTime{
calendar: calendar,
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: {microsecond, precision},
std_offset: 0,
utc_offset: 0,
zone_abbr: "UTC",
time_zone: "Etc/UTC"
}
{:ok, datetime}
end
defp shift_zone_for_iso_days_utc(iso_days_utc, calendar, precision, time_zone, time_zone_db) do
case time_zone_db.time_zone_period_from_utc_iso_days(iso_days_utc, time_zone) do
{:ok, %{std_offset: std_offset, utc_offset: utc_offset, zone_abbr: zone_abbr}} ->
@@ -1177,7 +1202,7 @@ defmodule DateTime do
datetime
|> to_iso_days()
# Subtract total original offset in order to get UTC and add the new offset
|> Calendar.ISO.add_day_fraction_to_iso_days(offset - total_offset, 86400)
|> Calendar.ISO.add_time_unit_to_iso_days(offset - total_offset, :second)
|> calendar.naive_datetime_from_iso_days()
end
@@ -1296,9 +1321,9 @@ defmodule DateTime do
end
@doc """
Converts a number of gregorian seconds to a `DateTime` struct.
Converts a number of Gregorian seconds to a `DateTime` struct.
The returned `DateTime` will have `UTC` timezone, if you want other timezone, please use
The returned `DateTime` will have `UTC` timezone, if you want another timezone, please use
`DateTime.shift_zone/3`.
## Examples
@@ -1341,7 +1366,7 @@ defmodule DateTime do
end
@doc """
Converts a `DateTime` struct to a number of gregorian seconds and microseconds.
Converts a `DateTime` struct to a number of Gregorian seconds and microseconds.
## Examples
@@ -1576,15 +1601,15 @@ defmodule DateTime do
def diff(datetime1, datetime2, unit \\ :second)
def diff(datetime1, datetime2, :day) do
diff(datetime1, datetime2, :second) |> div(86400)
diff(datetime1, datetime2, :microsecond) |> div(86_400_000_000)
end
def diff(datetime1, datetime2, :hour) do
diff(datetime1, datetime2, :second) |> div(3600)
diff(datetime1, datetime2, :microsecond) |> div(3_600_000_000)
end
def diff(datetime1, datetime2, :minute) do
diff(datetime1, datetime2, :second) |> div(60)
diff(datetime1, datetime2, :microsecond) |> div(60_000_000)
end
def diff(
@@ -1626,6 +1651,9 @@ defmodule DateTime do
all converted to microseconds. Negative values will move backwards
in time and the default precision is `:second`.
If the datetime is in the `"Etc/UTC"` time zone, this function
always succeeds without consulting the `time_zone_database`.
This function relies on a contiguous representation of time,
ignoring timezone changes. For example, if you add one day when there
are summer time/daylight saving time changes, it will also change the
@@ -1744,6 +1772,9 @@ defmodule DateTime do
Allowed units are: `:year`, `:month`, `:week`, `:day`, `:hour`, `:minute`, `:second`, `:microsecond`.
If the datetime is in the `"Etc/UTC"` time zone, this function
always succeeds without consulting the `time_zone_database`.
This operation is equivalent to shifting the datetime wall clock
(in other words, the value as someone in that timezone would see
on their watch), then applying the time zone offset to convert it
@@ -1812,44 +1843,6 @@ defmodule DateTime do
@spec shift(Calendar.datetime(), Duration.duration(), Calendar.time_zone_database()) :: t
def shift(datetime, duration, time_zone_database \\ Calendar.get_time_zone_database())
def shift(%{calendar: calendar, time_zone: "Etc/UTC"} = datetime, duration, _time_zone_database) do
%{
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
} = datetime
{year, month, day, hour, minute, second, microsecond} =
calendar.shift_naive_datetime(
year,
month,
day,
hour,
minute,
second,
microsecond,
__duration__!(duration)
)
%DateTime{
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond,
time_zone: "Etc/UTC",
zone_abbr: "UTC",
std_offset: 0,
utc_offset: 0
}
end
def shift(%{calendar: calendar} = datetime, duration, time_zone_database) do
%{
year: year,
@@ -2060,11 +2053,12 @@ defmodule DateTime do
end
defp apply_tz_offset(iso_days, offset) do
Calendar.ISO.add_day_fraction_to_iso_days(iso_days, -offset, 86400)
Calendar.ISO.add_time_unit_to_iso_days(iso_days, -offset, :second)
end
defp from_map(%{} = datetime_map) do
%DateTime{
calendar: datetime_map.calendar,
year: datetime_map.year,
month: datetime_map.month,
day: datetime_map.day,
+55 -41
View File
@@ -72,7 +72,7 @@ defmodule Duration do
However, once again, it is important to remember that shifting a duration is not
arithmetic, so you may want to use the functions in this module depending on what
you to achieve. Compare the results of both examples below:
you want to achieve. Compare the results of both examples below:
# Adding one month after the other
iex> date = ~D[2016-01-31]
@@ -129,6 +129,16 @@ defmodule Duration do
second: 0,
microsecond: {0, 0}
@typedoc """
The microsecond component of a duration.
Unlike `t:Calendar.microsecond/0`, the value may be negative, as
durations may represent negative amounts of time. The precision is
an integer from 0 to 6 holding the number of significant digits,
as in the calendar types.
"""
@type microsecond :: {value :: integer, precision :: 0..6}
@typedoc """
The duration struct type.
"""
@@ -140,7 +150,7 @@ defmodule Duration do
hour: integer,
minute: integer,
second: integer,
microsecond: Calendar.microsecond()
microsecond: microsecond()
}
@typedoc """
@@ -154,7 +164,7 @@ defmodule Duration do
| {:hour, integer}
| {:minute, integer}
| {:second, integer}
| {:microsecond, Calendar.microsecond()}
| {:microsecond, microsecond()}
@typedoc """
The duration type specifies a `%Duration{}` struct or a keyword list of valid duration unit pairs.
@@ -232,7 +242,7 @@ defmodule Duration do
@doc """
Adds units of given durations `d1` and `d2`.
Respects the the highest microsecond precision of the two.
Respects the highest microsecond precision of the two.
## Examples
@@ -243,26 +253,26 @@ defmodule Duration do
"""
@spec add(t, t) :: t
def add(%Duration{} = d1, %Duration{} = d2) do
{m1, p1} = d1.microsecond
{m2, p2} = d2.microsecond
def add(%Duration{microsecond: {ms1, p1}} = d1, %Duration{microsecond: {ms2, p2}} = d2) do
%{year: y1, month: mo1, week: w1, day: day1, hour: h1, minute: mi1, second: s1} = d1
%{year: y2, month: mo2, week: w2, day: day2, hour: h2, minute: mi2, second: s2} = d2
%Duration{
year: d1.year + d2.year,
month: d1.month + d2.month,
week: d1.week + d2.week,
day: d1.day + d2.day,
hour: d1.hour + d2.hour,
minute: d1.minute + d2.minute,
second: d1.second + d2.second,
microsecond: {m1 + m2, max(p1, p2)}
year: y1 + y2,
month: mo1 + mo2,
week: w1 + w2,
day: day1 + day2,
hour: h1 + h2,
minute: mi1 + mi2,
second: s1 + s2,
microsecond: {ms1 + ms2, max(p1, p2)}
}
end
@doc """
Subtracts units of given durations `d1` and `d2`.
Respects the the highest microsecond precision of the two.
Respects the highest microsecond precision of the two.
## Examples
@@ -273,19 +283,19 @@ defmodule Duration do
"""
@spec subtract(t, t) :: t
def subtract(%Duration{} = d1, %Duration{} = d2) do
{m1, p1} = d1.microsecond
{m2, p2} = d2.microsecond
def subtract(%Duration{microsecond: {ms1, p1}} = d1, %Duration{microsecond: {ms2, p2}} = d2) do
%{year: y1, month: mo1, week: w1, day: day1, hour: h1, minute: mi1, second: s1} = d1
%{year: y2, month: mo2, week: w2, day: day2, hour: h2, minute: mi2, second: s2} = d2
%Duration{
year: d1.year - d2.year,
month: d1.month - d2.month,
week: d1.week - d2.week,
day: d1.day - d2.day,
hour: d1.hour - d2.hour,
minute: d1.minute - d2.minute,
second: d1.second - d2.second,
microsecond: {m1 - m2, max(p1, p2)}
year: y1 - y2,
month: mo1 - mo2,
week: w1 - w2,
day: day1 - day2,
hour: h1 - h2,
minute: mi1 - mi2,
second: s1 - s2,
microsecond: {ms1 - ms2, max(p1, p2)}
}
end
@@ -302,14 +312,16 @@ defmodule Duration do
"""
@spec multiply(t, integer) :: t
def multiply(%Duration{microsecond: {ms, p}} = duration, integer) when is_integer(integer) do
%{year: y, month: mo, week: w, day: d, hour: h, minute: mi, second: s} = duration
%Duration{
year: duration.year * integer,
month: duration.month * integer,
week: duration.week * integer,
day: duration.day * integer,
hour: duration.hour * integer,
minute: duration.minute * integer,
second: duration.second * integer,
year: y * integer,
month: mo * integer,
week: w * integer,
day: d * integer,
hour: h * integer,
minute: mi * integer,
second: s * integer,
microsecond: {ms * integer, p}
}
end
@@ -327,14 +339,16 @@ defmodule Duration do
"""
@spec negate(t) :: t
def negate(%Duration{microsecond: {ms, p}} = duration) do
%{year: y, month: mo, week: w, day: d, hour: h, minute: mi, second: s} = duration
%Duration{
year: -duration.year,
month: -duration.month,
week: -duration.week,
day: -duration.day,
hour: -duration.hour,
minute: -duration.minute,
second: -duration.second,
year: -y,
month: -mo,
week: -w,
day: -d,
hour: -h,
minute: -mi,
second: -s,
microsecond: {-ms, p}
}
end
+141 -102
View File
@@ -33,8 +33,9 @@ defmodule Calendar.ISO do
you to format datetimes however else you desire.
Elixir does not support reduced accuracy formats (for example, a date without
the day component) nor decimal precisions in the lowest component (such as
`10:01:25,5`).
the day component) nor decimal precisions in components other than seconds
(such as `10:01,5`). Fractional seconds are supported and truncated to
microsecond precision.
#### Examples
@@ -81,6 +82,11 @@ defmodule Calendar.ISO do
iex> Calendar.ISO.parse_time("23")
{:error, :invalid_format}
A decimal fraction is accepted on seconds, but not on any other component:
iex> Calendar.ISO.parse_time("10:01:25,5")
{:ok, {10, 1, 25, {500000, 1}}}
### Extensions
The parser and formatter adopt one ISO 8601 extension: extended year notation.
@@ -417,16 +423,13 @@ defmodule Calendar.ISO do
@spec parse_date(String.t(), format) ::
{:ok, {year, month, day}}
| {:error, atom}
def parse_date(string, format) when is_binary(string) and is_format(format),
do: parse_date_guarded(string, format)
defp parse_date_guarded("-" <> string, format),
def parse_date("-" <> string, format) when is_format(format),
do: do_parse_date(string, -1, format)
defp parse_date_guarded("+" <> string, format),
def parse_date("+" <> string, format) when is_format(format),
do: do_parse_date(string, 1, format)
defp parse_date_guarded(string, format),
def parse_date(string, format) when is_binary(string) and is_format(format),
do: do_parse_date(string, 1, format)
defp do_parse_date(unquote(match_basic_date), multiplier, :basic) when unquote(guard_date) do
@@ -502,16 +505,13 @@ defmodule Calendar.ISO do
@spec parse_naive_datetime(String.t(), format) ::
{:ok, {year, month, day, hour, minute, second, microsecond}}
| {:error, atom}
def parse_naive_datetime(string, format) when is_binary(string) and is_format(format),
do: parse_naive_datetime_guarded(string, format)
defp parse_naive_datetime_guarded("-" <> string, format),
def parse_naive_datetime("-" <> string, format) when is_format(format),
do: do_parse_naive_datetime(string, -1, format)
defp parse_naive_datetime_guarded("+" <> string, format),
def parse_naive_datetime("+" <> string, format) when is_format(format),
do: do_parse_naive_datetime(string, 1, format)
defp parse_naive_datetime_guarded(string, format),
def parse_naive_datetime(string, format) when is_binary(string) and is_format(format),
do: do_parse_naive_datetime(string, 1, format)
defp do_parse_naive_datetime(
@@ -606,16 +606,13 @@ defmodule Calendar.ISO do
@spec parse_utc_datetime(String.t(), format) ::
{:ok, {year, month, day, hour, minute, second, microsecond}, utc_offset}
| {:error, atom}
def parse_utc_datetime(string, format) when is_binary(string) and is_format(format),
do: parse_utc_datetime_guarded(string, format)
defp parse_utc_datetime_guarded("-" <> string, format),
def parse_utc_datetime("-" <> string, format) when is_format(format),
do: do_parse_utc_datetime(string, -1, format)
defp parse_utc_datetime_guarded("+" <> string, format),
def parse_utc_datetime("+" <> string, format) when is_format(format),
do: do_parse_utc_datetime(string, 1, format)
defp parse_utc_datetime_guarded(string, format),
def parse_utc_datetime(string, format) when is_binary(string) and is_format(format),
do: do_parse_utc_datetime(string, 1, format)
defp do_parse_utc_datetime(
@@ -666,12 +663,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_time_unit_to_iso_days({0, day_fraction}, -offset, :second) do
{0, day_fraction} ->
{{year, month, day}, time_from_day_fraction(day_fraction)}
{extra_days, day_fraction} ->
base_days = date_to_iso_days(year, month, day)
base_days = valid_date_to_iso_days(year, month, day)
{date_from_iso_days(base_days + extra_days), time_from_day_fraction(day_fraction)}
end
@@ -683,75 +680,73 @@ defmodule Calendar.ISO do
end
@doc """
Parses an ISO 8601 formatted duration string to a list of `Duration` compabitble unit pairs.
Parses an ISO 8601 formatted duration string to a list of `Duration` compatible unit pairs.
See `Duration.from_iso8601/1`.
"""
@doc since: "1.17.0"
@spec parse_duration(String.t()) :: {:ok, [Duration.unit_pair()]} | {:error, atom}
def parse_duration("P" <> string) when byte_size(string) > 0 do
parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D)
parse_duration_date(string, 1, [], 0)
end
def parse_duration("+P" <> string) when byte_size(string) > 0 do
parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D)
parse_duration_date(string, 1, [], 0)
end
def parse_duration("-P" <> string) when byte_size(string) > 0 do
with {:ok, fields} <- parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D) do
{:ok,
Enum.map(fields, fn
{:microsecond, {value, precision}} -> {:microsecond, {-value, precision}}
{unit, value} -> {unit, -value}
end)}
end
parse_duration_date(string, -1, [], 0)
end
def parse_duration(_) do
{:error, :invalid_duration}
end
defp parse_duration_date("", acc, _allowed), do: {:ok, acc}
defp parse_duration_date("", _sign, acc, _min_position), do: {:ok, acc}
defp parse_duration_date("T" <> string, acc, _allowed) when byte_size(string) > 0 do
parse_duration_time(string, acc, hour: ?H, minute: ?M, second: ?S)
defp parse_duration_date("T" <> string, sign, acc, _min_position)
when byte_size(string) > 0 do
parse_duration_time(string, sign, acc, 0)
end
defp parse_duration_date(string, acc, allowed) do
with {integer, <<next, rest::binary>>} <- Integer.parse(string),
{key, allowed} <- find_unit(allowed, next) do
parse_duration_date(rest, [{key, integer} | acc], allowed)
defp parse_duration_date(string, sign, acc, min_position) do
with {integer, <<unit, rest::binary>>} <- Integer.parse(string),
{key, next_min_position} <- find_date_unit(min_position, unit) do
parse_duration_date(rest, sign, [{key, integer * sign} | acc], next_min_position)
else
_ -> {:error, :invalid_date_component}
end
end
defp parse_duration_time("", acc, _allowed), do: {:ok, acc}
defp parse_duration_time("", _sign, acc, _min_position), do: {:ok, acc}
defp parse_duration_time(string, acc, allowed) do
defp parse_duration_time(string, sign, acc, min_position) do
case Integer.parse(string) do
{second, <<delimiter, _::binary>> = rest} when delimiter in [?., ?,] ->
case parse_microsecond(rest) do
{{ms, precision}, "S"} ->
ms =
case string do
"-" <> _ ->
-ms
with {:second, _next_min_position} <- find_time_unit(min_position, ?S),
{{ms, precision}, "S"} <- parse_microsecond(rest) do
ms =
case string do
"-" <> _ ->
-ms
_ ->
ms
end
{:ok, [second: second, microsecond: {ms, precision}] ++ acc}
_ ->
ms
end
{:ok, [second: second * sign, microsecond: {ms * sign, precision}] ++ acc}
else
_ ->
{:error, :invalid_time_component}
end
{integer, <<next, rest::binary>>} ->
case find_unit(allowed, next) do
{key, allowed} -> parse_duration_time(rest, [{key, integer} | acc], allowed)
false -> {:error, :invalid_time_component}
{integer, <<unit, rest::binary>>} ->
case find_time_unit(min_position, unit) do
{key, next_min_position} ->
parse_duration_time(rest, sign, [{key, integer * sign} | acc], next_min_position)
false ->
{:error, :invalid_time_component}
end
_ ->
@@ -759,9 +754,17 @@ defmodule Calendar.ISO do
end
end
defp find_unit([{key, unit} | rest], unit), do: {key, rest}
defp find_unit([_ | rest], unit), do: find_unit(rest, unit)
defp find_unit([], _unit), do: false
# The minimum position is the earliest unit still allowed, or one past the end.
defp find_date_unit(min_position, ?Y) when min_position <= 0, do: {:year, 1}
defp find_date_unit(min_position, ?M) when min_position <= 1, do: {:month, 2}
defp find_date_unit(min_position, ?W) when min_position <= 2, do: {:week, 3}
defp find_date_unit(min_position, ?D) when min_position <= 3, do: {:day, 4}
defp find_date_unit(_min_position, _unit), do: false
defp find_time_unit(min_position, ?H) when min_position <= 0, do: {:hour, 1}
defp find_time_unit(min_position, ?M) when min_position <= 1, do: {:minute, 2}
defp find_time_unit(min_position, ?S) when min_position <= 2, do: {:second, 3}
defp find_time_unit(_min_position, _unit), do: false
@doc """
Returns the `t:Calendar.iso_days/0` format of the specified date.
@@ -892,12 +895,15 @@ defmodule Calendar.ISO do
# Converts year, month, day to count of days since 0000-01-01.
@doc false
def date_to_iso_days(0, 1, 1), do: 0
def date_to_iso_days(1970, 1, 1), do: @unix_epoch_days
def date_to_iso_days(year, month, day) do
ensure_day_in_month!(year, month, day)
valid_date_to_iso_days(year, month, day)
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
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
@@ -1131,6 +1137,11 @@ defmodule Calendar.ISO do
It is an integer from 1 to 4.
In the ISO calendar, the quarter is determined solely by the month, so the `year`
and `day` arguments are ignored. Combination of `year`, `month`, and `day` is not
validated as a valid date, unlike in `day_of_year/3`. Use `valid_date?/3` when
full date validation is required.
## Examples
iex> Calendar.ISO.quarter_of_year(2016, 1, 31)
@@ -1173,7 +1184,7 @@ defmodule Calendar.ISO do
@doc since: "1.8.0"
@spec year_of_era(year) :: {1..10_000, 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}
def year_of_era(year) when is_year_BCE(year), do: {1 - year, 0}
@doc """
Calendar callback to compute the year and era from the
@@ -1226,7 +1237,7 @@ defmodule Calendar.ISO do
end
def day_of_era(year, month, day) when is_year_BCE(year) do
day = abs(date_to_iso_days(year, month, day) - @iso_epoch)
day = @iso_epoch - date_to_iso_days(year, month, day)
{day, 0}
end
@@ -1453,7 +1464,7 @@ defmodule Calendar.ISO do
@doc """
Converts the given naive_datetime into a iodata.
See `naive_datetime_to_iodata/8` for more information.
See `naive_datetime_to_string/8` for more information.
## Examples
@@ -1578,7 +1589,7 @@ defmodule Calendar.ISO do
@doc """
Converts the given datetime into a iodata.
See `datetime_to_iodata/12` for more information.
See `datetime_to_string/12` for more information.
## Examples
@@ -1682,7 +1693,7 @@ defmodule Calendar.ISO do
end
@doc """
Determines if the date given is valid according to the proleptic Gregorian calendar.
Determines if the time given is valid.
Leap seconds are not supported by the built-in Calendar.ISO.
@@ -1702,7 +1713,7 @@ defmodule Calendar.ISO do
boolean
def valid_time?(hour, minute, second, {ms_value, ms_precision} = _microsecond)
when is_integer(hour) and is_integer(minute) and is_integer(second) and is_integer(ms_value) and
is_integer(ms_value) do
is_integer(ms_precision) do
is_hour(hour) and is_minute(minute) and is_second(second) and
is_microsecond(ms_value, ms_precision)
end
@@ -1835,6 +1846,9 @@ defmodule Calendar.ISO do
shift_options = shift_datetime_options(duration)
Enum.reduce(shift_options, {year, month, day, hour, minute, second, microsecond}, fn
{:microsecond, {0, _}}, naive_datetime ->
naive_datetime
{_, 0}, naive_datetime ->
naive_datetime
@@ -1864,6 +1878,9 @@ defmodule Calendar.ISO do
shift_options = shift_time_options(duration)
Enum.reduce(shift_options, {hour, minute, second, microsecond}, fn
{:microsecond, {0, _}}, time ->
time
{_, 0}, time ->
time
@@ -1875,7 +1892,7 @@ defmodule Calendar.ISO do
@doc false
def shift_days({year, month, day}, days) do
{year, month, day} =
date_to_iso_days(year, month, day)
valid_date_to_iso_days(year, month, day)
|> Kernel.+(days)
|> date_from_iso_days()
@@ -1926,8 +1943,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)
add_day_fraction_to_iso_days(iso_days, value, ppd)
add_time_unit_to_iso_days(iso_days, value, unit)
end
defp shift_time_unit_values({0, _}, {_, original_precision}) do
@@ -2008,8 +2024,8 @@ defmodule Calendar.ISO do
total = System.convert_time_unit(integer, unit, :microsecond)
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, microseconds} = div_rem(total, @microseconds_per_second)
seconds = @unix_epoch + seconds
precision = precision_for_unit(unit)
{date, time} = iso_seconds_to_datetime(seconds)
{:ok, date, time, {microseconds, precision}}
@@ -2030,38 +2046,27 @@ defmodule Calendar.ISO do
end
end
defp parse_microsecond("." <> rest) do
case parse_microsecond(rest, 0, []) do
{[], 0, _} ->
:error
defp parse_microsecond("." <> rest), do: parse_microsecond(rest, rest, 0)
defp parse_microsecond("," <> rest), do: parse_microsecond(rest, rest, 0)
defp parse_microsecond(rest), do: {{0, 0}, rest}
{microsecond, precision, rest} ->
scale = scale_factor(precision)
{{:erlang.list_to_integer(microsecond) * scale, precision}, rest}
end
end
# Digits past the sixth are consumed but do not contribute to the value.
defp parse_microsecond(<<head, tail::binary>>, digits, 6) when head in ?0..?9,
do: parse_microsecond(tail, digits, 6)
defp parse_microsecond("," <> rest) do
parse_microsecond("." <> rest)
end
defp parse_microsecond(<<head, tail::binary>>, digits, precision) when head in ?0..?9,
do: parse_microsecond(tail, digits, precision + 1)
defp parse_microsecond(rest) do
{{0, 0}, rest}
end
defp parse_microsecond(_rest, _digits, 0), do: :error
defp parse_microsecond(<<head, tail::binary>>, 6, acc) when head in ?0..?9,
do: parse_microsecond(tail, 6, acc)
defp parse_microsecond(<<head, tail::binary>>, precision, acc) when head in ?0..?9,
do: parse_microsecond(tail, precision + 1, [head | acc])
defp parse_microsecond(rest, precision, acc) do
{:lists.reverse(acc), precision, rest}
defp parse_microsecond(rest, digits, precision) do
scale = scale_factor(precision)
microsecond = :erlang.binary_to_integer(:binary.part(digits, 0, precision)) * scale
{{microsecond, precision}, rest}
end
defp parse_offset(""), do: {nil, ""}
defp parse_offset("Z"), do: {0, ""}
defp parse_offset("-00:00"), do: :error
defp parse_offset(<<?+, h1, h2, ?:, m1, m2, rest::binary>>),
do: parse_offset(1, h1, h2, m1, m2, rest)
@@ -2084,7 +2089,8 @@ defmodule Calendar.ISO do
true <- m1 in ?0..?5 and m2 in ?0..?9,
hour = (h1 - ?0) * 10 + h2 - ?0,
min = (m1 - ?0) * 10 + m2 - ?0,
true <- hour < 24 do
true <- hour < 24,
true <- sign == 1 or hour != 0 or min != 0 do
{(hour * 60 + min) * 60 * sign, rest}
else
_ -> :error
@@ -2100,10 +2106,43 @@ defmodule Calendar.ISO do
end
@doc false
def iso_days_to_unit({days, {parts, ppd}}, unit) do
day_microseconds = days * @parts_per_day
microseconds = divide_by_parts_per_day(parts, ppd)
System.convert_time_unit(day_microseconds + microseconds, :microsecond, unit)
def iso_days_to_unit(iso_days, :second) do
floor_div_positive_divisor(iso_days_to_microseconds(iso_days), @microseconds_per_second)
end
def iso_days_to_unit(iso_days, :millisecond) do
floor_div_positive_divisor(iso_days_to_microseconds(iso_days), 1_000)
end
def iso_days_to_unit(iso_days, :microsecond) do
iso_days_to_microseconds(iso_days)
end
def iso_days_to_unit(iso_days, unit) do
System.convert_time_unit(iso_days_to_microseconds(iso_days), :microsecond, unit)
end
defp iso_days_to_microseconds({days, {parts, ppd}}) do
days * @parts_per_day + divide_by_parts_per_day(parts, ppd)
end
@doc false
def add_time_unit_to_iso_days(iso_days, add, :second) do
add_day_fraction_to_iso_days(iso_days, add * @microseconds_per_second, @parts_per_day)
end
def add_time_unit_to_iso_days(iso_days, add, :millisecond) do
add_day_fraction_to_iso_days(iso_days, add * 1_000, @parts_per_day)
end
def add_time_unit_to_iso_days(iso_days, add, :microsecond) do
add_day_fraction_to_iso_days(iso_days, add, @parts_per_day)
end
def add_time_unit_to_iso_days(iso_days, add, unit)
when unit == :nanosecond or is_integer(unit) do
ppd = System.convert_time_unit(@seconds_per_day, :second, unit)
add_day_fraction_to_iso_days(iso_days, add, ppd)
end
@doc false
+46 -38
View File
@@ -164,7 +164,7 @@ defmodule NaiveDateTime do
Returns the "local time" for the machine the Elixir program is running on.
WARNING: This function can cause insidious bugs. It depends on the time zone
configuration at run time. This can changed and be set to a time zone that has
configuration at run time. This can change and be set to a time zone that has
daylight saving jumps (spring forward or fall back).
This function can be used to display what the time is right now for the time
@@ -541,15 +541,15 @@ defmodule NaiveDateTime do
def diff(naive_datetime1, naive_datetime2, unit \\ :second)
def diff(naive_datetime1, naive_datetime2, :day) do
diff(naive_datetime1, naive_datetime2, :second) |> div(86400)
diff(naive_datetime1, naive_datetime2, :microsecond) |> div(86_400_000_000)
end
def diff(naive_datetime1, naive_datetime2, :hour) do
diff(naive_datetime1, naive_datetime2, :second) |> div(3600)
diff(naive_datetime1, naive_datetime2, :microsecond) |> div(3_600_000_000)
end
def diff(naive_datetime1, naive_datetime2, :minute) do
diff(naive_datetime1, naive_datetime2, :second) |> div(60)
diff(naive_datetime1, naive_datetime2, :microsecond) |> div(60_000_000)
end
def diff(
@@ -570,9 +570,11 @@ defmodule NaiveDateTime do
"unsupported time unit. Expected :day, :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}"
end
units1 = naive_datetime1 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit)
units2 = naive_datetime2 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit)
units1 - units2
diff_microsecond =
(naive_datetime1 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(:microsecond)) -
(naive_datetime2 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(:microsecond))
System.convert_time_unit(diff_microsecond, :microsecond, unit)
end
@doc """
@@ -672,7 +674,7 @@ defmodule NaiveDateTime do
"""
@doc since: "1.6.0"
@spec truncate(t(), :microsecond | :millisecond | :second) :: t()
@spec truncate(Calendar.naive_datetime(), :microsecond | :millisecond | :second) :: t()
def truncate(%NaiveDateTime{microsecond: microsecond} = naive_datetime, precision) do
%{naive_datetime | microsecond: Calendar.truncate(microsecond, precision)}
end
@@ -715,16 +717,18 @@ 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: _
}) do
def to_date(
%{
year: year,
month: month,
day: day,
calendar: calendar,
hour: _,
minute: _,
second: _,
microsecond: _
} = _naive_datetime
) do
%Date{year: year, month: month, day: day, calendar: calendar}
end
@@ -741,16 +745,18 @@ 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
}) do
def to_time(
%{
year: _,
month: _,
day: _,
calendar: calendar,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
} = _naive_datetime
) do
%Time{
hour: hour,
minute: minute,
@@ -1146,16 +1152,18 @@ 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}
}) do
def to_gregorian_seconds(
%{
calendar: calendar,
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: {microsecond, precision}
} = _naive_datetime
) do
{days, day_fraction} =
calendar.naive_datetime_to_iso_days(
year,
+41 -18
View File
@@ -62,7 +62,7 @@ defmodule Time do
You can pass a time unit to automatically truncate the resulting time.
The default unit if none gets passed is `:native` which results on a default resolution of microseconds.
The default unit if none gets passed is `:native` which results in a default resolution of microseconds.
## Examples
@@ -146,8 +146,9 @@ defmodule Time do
iex> Time.new(23, 59, 59, 1_000_000)
{:error, :invalid_time}
# Invalid precision
Time.new(23, 59, 59, {999_999, 10})
Invalid precision:
iex> Time.new(23, 59, 59, {999_999, 10})
{:error, :invalid_time}
"""
@@ -466,8 +467,12 @@ defmodule Time do
Calendar.microsecond(),
Calendar.calendar()
) :: t
def from_seconds_after_midnight(seconds, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
when is_integer(seconds) do
def from_seconds_after_midnight(
seconds,
{microsecond, precision} \\ {0, 0},
calendar \\ Calendar.ISO
)
when is_integer(seconds) and microsecond in 0..999_999 and precision in 0..6 do
seconds_in_day = Integer.mod(seconds, @seconds_per_day)
{hour, minute, second, {_, _}} =
@@ -478,7 +483,7 @@ defmodule Time do
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
microsecond: {microsecond, precision}
}
end
@@ -496,7 +501,7 @@ defmodule Time do
"""
@doc since: "1.11.0"
@spec to_seconds_after_midnight(Calendar.time()) :: {integer(), non_neg_integer()}
@spec to_seconds_after_midnight(Calendar.time()) :: {non_neg_integer(), non_neg_integer()}
def to_seconds_after_midnight(%{microsecond: {microsecond, _precision}} = time) do
iso_days = {0, to_day_fraction(time)}
{Calendar.ISO.iso_days_to_unit(iso_days, :second), microsecond}
@@ -689,7 +694,7 @@ defmodule Time do
@doc """
Compares two time structs.
Returns `:gt` if first time is later than the second
Returns `:gt` if the first time is later than the second
and `:lt` for vice versa. If the two times are equal
`:eq` is returned.
@@ -715,14 +720,32 @@ defmodule Time do
"""
@doc since: "1.4.0"
@spec compare(Calendar.time(), Calendar.time()) :: :lt | :eq | :gt
def compare(%{calendar: calendar} = time1, %{calendar: calendar} = time2) do
%{hour: hour1, minute: minute1, second: second1, microsecond: {microsecond1, _}} = time1
%{hour: hour2, minute: minute2, second: second2, microsecond: {microsecond2, _}} = time2
case {{hour1, minute1, second1, microsecond1}, {hour2, minute2, second2, microsecond2}} do
{first, second} when first > second -> :gt
{first, second} when first < second -> :lt
_ -> :eq
def compare(
%{
hour: hour1,
minute: minute1,
second: second1,
microsecond: {microsecond1, _},
calendar: calendar
},
%{
hour: hour2,
minute: minute2,
second: second2,
microsecond: {microsecond2, _},
calendar: calendar
}
) do
cond do
hour1 > hour2 -> :gt
hour1 < hour2 -> :lt
minute1 > minute2 -> :gt
minute1 < minute2 -> :lt
second1 > second2 -> :gt
second1 < second2 -> :lt
microsecond1 > microsecond2 -> :gt
microsecond1 < microsecond2 -> :lt
true -> :eq
end
end
@@ -901,11 +924,11 @@ defmodule Time do
def diff(time1, time2, unit \\ :second)
def diff(time1, time2, :hour) do
diff(time1, time2, :second) |> div(3600)
diff(time1, time2, :microsecond) |> div(3_600_000_000)
end
def diff(time1, time2, :minute) do
diff(time1, time2, :second) |> div(60)
diff(time1, time2, :microsecond) |> div(60_000_000)
end
def diff(
+160 -89
View File
@@ -50,7 +50,7 @@ defmodule Code do
You can use `ensure_loaded/1` (as well as `ensure_loaded?/1` and
`ensure_loaded!/1`) to check if a module is loaded before using it and
act.
act accordingly.
## `ensure_compiled/1` and `ensure_compiled!/1`
@@ -258,6 +258,7 @@ defmodule Code do
| {:locals_without_parens, keyword()}
| {:force_do_end_blocks, boolean()}
| {:migrate, boolean()}
| {:migrate_atom_interpolations, boolean()}
| {:migrate_bitstring_modifiers, boolean()}
| {:migrate_call_parens_on_pipe, boolean()}
| {:migrate_charlists_as_sigils, boolean()}
@@ -272,6 +273,7 @@ defmodule Code do
| {:escape, boolean()}
| {:locals_without_parens, keyword()}
| {:comments, [term()]}
| {:syntax_colors, [{Inspect.Opts.color_key(), IO.ANSI.ansidata()}]}
@typedoc """
Options for parsing functions that convert strings to quoted expressions.
@@ -285,20 +287,26 @@ defmodule Code do
unescape: boolean(),
existing_atoms_only: boolean(),
token_metadata: boolean(),
literal_encoder: (term(), Macro.metadata() -> term()),
static_atoms_encoder: (atom() -> term()),
literal_encoder: (term(), Macro.metadata() -> {:ok, Macro.t()} | {:error, binary()}),
static_atoms_encoder: (binary(), Macro.metadata() -> {:ok, term()} | {:error, binary()}),
emit_warnings: boolean()
]
@typedoc """
Options for environment evaluation functions like eval_string/3 and eval_quoted/3.
Options for evaluation environment, accepted by `env_for_eval/1`.
"""
@type env_eval_opts :: [
file: binary(),
line: pos_integer(),
module: module(),
prune_binding: boolean()
]
@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()}}
@boolean_compiler_options [
:docs,
@@ -308,11 +316,16 @@ defmodule Code do
:relative_paths
]
@list_compiler_options [:tracers, :parser_options]
@list_compiler_options [:tracers, :parser_options, :erlc_options]
@available_compiler_options @boolean_compiler_options ++
@list_compiler_options ++
[:on_undefined_variable, :infer_signatures, :no_warn_undefined]
[
:on_undefined_variable,
:infer_signatures,
:no_warn_undefined,
:module_definition
]
@doc """
Lists all required files.
@@ -402,7 +415,7 @@ defmodule Code do
operations.
"""
@spec append_path(Path.t(), cache: boolean()) :: true | false
@spec append_path(Path.t(), cache: boolean()) :: boolean()
def append_path(path, opts \\ []) do
apply(:code, :add_pathz, [to_charlist(Path.expand(path)) | cache(opts)]) == true
end
@@ -541,8 +554,7 @@ defmodule Code do
This is the list of directories the Erlang VM uses for finding
module code. The list of files is managed per Erlang VM node.
The path is expanded with `Path.expand/1` before being deleted. If the
path does not exist, this function returns `false`.
All paths are expanded with `Path.expand/1` before being deleted.
"""
@doc since: "1.15.0"
@spec delete_paths([Path.t()]) :: :ok
@@ -568,9 +580,11 @@ defmodule Code do
## Options
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.
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
Returns a tuple of the form `{value, binding}`, where `value` is the value
returned from evaluating `string`. If an error occurs while evaluating
@@ -600,7 +614,7 @@ defmodule Code do
iex> Enum.sort(binding)
[a: 3, b: 2]
For convenience, you can pass `__ENV__/0` as the `opts` argument and
For convenience, you can pass `__ENV__/0` as the `opts_or_env` argument and
all imports, requires and aliases defined in the current environment
will be automatically carried over:
@@ -612,21 +626,28 @@ defmodule Code do
[a: 1, b: 2]
"""
@spec eval_string(List.Chars.t(), binding, Macro.Env.t() | env_eval_opts) :: {term, binding}
def eval_string(string, binding \\ [], opts \\ [])
@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 \\ [])
def eval_string(string, binding, %Macro.Env{} = env) do
validated_eval_string(string, binding, env)
validated_eval_string(string, validate_binding(binding), env_for_eval(env), [])
end
def eval_string(string, binding, opts) when is_list(opts) do
validated_eval_string(string, binding, opts)
validated_eval_string(string, validate_binding(binding), env_for_eval(opts), opts)
end
defp validated_eval_string(string, binding, opts_or_env) do
%{line: line, file: file} = env = env_for_eval(opts_or_env)
defp validate_binding(binding) 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
forms = :elixir.string_to_quoted!(to_charlist(string), line, 1, file, [])
{value, binding, _env} = eval_verify(:eval_forms, [forms, binding, env])
{value, binding, _env} = eval_verify(:eval_forms, [forms, binding, env, opts])
{value, binding}
end
@@ -747,6 +768,12 @@ defmodule Code do
* `:migrate` (since v1.18.0) - when `true`, sets all other migration options
to `true` by default. Defaults to `false`.
* `:migrate_atom_interpolations` (since v1.21.0) - when `true`, rewrites
deprecated atom interpolations to explicit calls to `String.to_unsafe_atom/1`.
For example, `:"foo_#{bar}"` becomes `String.to_unsafe_atom("foo_#{bar}")`.
Interpolated keywords like `["foo_#{bar}": 1]` are **not** migrated.
Defaults to the value of the `:migrate` option. This option changes the AST.
* `:migrate_bitstring_modifiers` (since v1.18.0) - when `true`,
removes unnecessary parentheses in known bitstring
[modifiers](`<<>>/1`), for example `<<foo::binary()>>`
@@ -1129,7 +1156,8 @@ defmodule Code do
returned quoted expressions (instead of evaluated).
See `eval_string/3` for a description of arguments and return types.
The options are described under `env_for_eval/1`.
It accepts the same options as both `env_for_eval/1` and
`eval_quoted_with_env/4`.
## Examples
@@ -1151,11 +1179,20 @@ defmodule Code do
[a: 1, b: 2]
"""
@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)])
@spec eval_quoted(Macro.t(), binding, Macro.Env.t() | [eval_opt | env_eval_opt]) ::
{term, binding}
def eval_quoted(quoted, binding \\ [], 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
@@ -1183,14 +1220,9 @@ 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_opts) :: Macro.Env.t()
@spec env_for_eval(Macro.Env.t() | [env_eval_opt]) :: Macro.Env.t()
def env_for_eval(env_or_opts), do: :elixir.env_for_eval(env_or_opts)
@doc """
@@ -1204,11 +1236,19 @@ defmodule Code do
## Options
It accepts the same options as `env_for_eval/1`.
* `: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.
"""
@doc since: "1.14.0"
@spec eval_quoted_with_env(Macro.t(), binding, Macro.Env.t(), env_eval_opts) ::
@spec eval_quoted_with_env(Macro.t(), binding, Macro.Env.t(), [eval_opt]) ::
{term, binding, Macro.Env.t()}
def eval_quoted_with_env(quoted, binding, %Macro.Env{} = env, opts \\ [])
when is_list(binding) do
@@ -1258,9 +1298,9 @@ defmodule Code do
* `:literal_encoder` (since v1.10.0) - how to encode literals in the AST.
It must be a function that receives two arguments, the literal and its
metadata, and it must return `{:ok, ast :: Macro.t}` or
`{:error, reason :: binary}`. If you return anything than the literal
`{:error, reason :: binary}`. If you return anything other than the literal
itself as the `term`, then the AST is no longer valid. This option
may still useful for textual analysis of the source code.
may still be useful for textual analysis of the source code.
* `:static_atoms_encoder` - the static atom encoder function, see
"The `:static_atoms_encoder` function" section below. Note this
@@ -1286,7 +1326,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 file, line and column. It must return
keyword list with the current line and column. It must return
`{:ok, token :: term} | {:error, reason :: binary}`.
The encoder function is supposed to create an atom from the given
@@ -1325,14 +1365,7 @@ defmodule Code do
file = Keyword.get(opts, :file, "nofile")
line = Keyword.get(opts, :line, 1)
column = Keyword.get(opts, :column, 1)
case :elixir.string_to_tokens(to_charlist(string), line, column, file, opts) do
{:ok, tokens} ->
:elixir.tokens_to_quoted(tokens, file, opts)
{:error, _error_msg} = error ->
error
end
:elixir.string_to_quoted(to_charlist(string), line, column, file, opts)
end
@doc """
@@ -1361,12 +1394,15 @@ defmodule Code do
while preserving information like comments and literals position.
Returns `{:ok, quoted_form, comments}` if it succeeds,
`{:error, {line, error, token}}` otherwise.
`{:error, {location, error, token}}` otherwise, where `location`
is keyword metadata containing the line and column of the error.
Comments are maps with the following fields:
* `:line` - The line number of the source code
* `:column` - The column number of the source code
* `:text` - The full text of the comment, including the leading `#`
* `:previous_eol_count` - How many end of lines there are between the comment and the previous AST node or comment
@@ -1408,8 +1444,7 @@ defmodule Code do
Process.put(:code_formatter_comments, [])
opts = [preserve_comments: &preserve_comments/5] ++ opts
with {:ok, tokens} <- :elixir.string_to_tokens(charlist, line, column, file, opts),
{:ok, forms} <- :elixir.tokens_to_quoted(tokens, file, opts) do
with {:ok, forms} <- :elixir.string_to_quoted(charlist, line, column, file, opts) do
comments = Enum.reverse(Process.get(:code_formatter_comments))
{:ok, forms, comments}
end
@@ -1422,7 +1457,9 @@ defmodule Code do
Returns the AST and a list of comments if it succeeds, raises an exception
otherwise. The exception is a `TokenMissingError` in case a token is missing
(usually because the expression is incomplete), `SyntaxError` otherwise.
(usually because the expression is incomplete), `MismatchedDelimiterError`
(in case of mismatched opening and closing delimiters) and `SyntaxError`
otherwise.
Check `string_to_quoted/2` for options information.
"""
@@ -1525,9 +1562,13 @@ defmodule Code do
`string_to_quoted/2`, setting this option to `false` will prevent it from
escaping the sequences twice. Defaults to `true`.
* `:syntax_colors` - a keyword list of colors the output is colorized.
See `Inspect.Opts` for more information.
See `format_string!/2` for the full list of formatting options including
`:file`, `:line`, `:line_length`, `:locals_without_parens`, `:force_do_end_blocks`,
`:syntax_colors`, and all migration options like `:migrate_charlists_as_sigils`.
`:file`, `:line`, `:locals_without_parens`, `:force_do_end_blocks`, and all
migration options like `:migrate_charlists_as_sigils`. Note `:line_length`
does not apply here.
"""
@doc since: "1.13.0"
@spec quoted_to_algebra(Macro.t(), [format_opt() | quoted_to_algebra_opt()]) ::
@@ -1609,13 +1650,19 @@ defmodule Code do
nil
:proceed ->
loaded =
Module.ParallelChecker.verify(fn ->
:elixir_compiler.string(charlist, file, fn _, _ -> :ok end)
end)
try do
loaded =
Module.ParallelChecker.verify(fn ->
:elixir_compiler.string(charlist, file, fn _, _ -> :ok end)
end)
:elixir_code_server.cast({:required, file})
loaded
:elixir_code_server.cast({:required, file})
loaded
catch
kind, reason ->
:elixir_code_server.call({:release, file})
:erlang.raise(kind, reason, __STACKTRACE__)
end
end
end
@@ -1641,7 +1688,7 @@ defmodule Code do
@doc """
Stores all given compilation options.
Changing the compilation options affect all processes
Changing the compilation options affects all processes
running in a given Erlang VM node. To store individual
options and for a description of all options, see
`put_compiler_option/2`.
@@ -1705,14 +1752,11 @@ defmodule Code do
@doc """
Stores a compilation option.
Changing the compilation options affect all processes running in a
Changing the compilation options affects all processes running in a
given Erlang VM node.
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`.
@@ -1724,10 +1768,17 @@ defmodule Code do
remove the `:debug_info` while deploying, tools like `mix release`
already do such by default.
Other environments, such as `mix test`, automatically disables this
Other environments, such as `mix test`, automatically disable this
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`.
* `:erlc_options` (since v1.21.0) - a list of Erlang compiler options. For example,
`erlc_options: [:beam_debug_info, :beam_debug_stack]` emits Erlang/OTP
debug metadata for BEAM debuggers. Defaults to `[]`.
* `:ignore_already_consolidated` (since v1.10.0) - when `true`, does not warn
when a protocol has already been consolidated and a new implementation is added.
Defaults to `false`.
@@ -1735,28 +1786,41 @@ defmodule Code do
* `:ignore_module_conflict` - when `true`, does not warn when a module has
already been defined. Defaults to `false`.
* `:infer_signatures` (since v1.18.0) - a list of applications of which modules
should be using during type inference. When `false`, it disables module-local
* `:infer_signatures` (since v1.18.0) - a list of applications whose modules
should be used 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.
Defaults to `true`, which is equivalent to setting it to `[:elixir]` only.
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
signatures for test files). Outside of Mix projects, it defaults to `[:elixir]`.
When setting this option, we recommend running `mix clean` so the modules can be
recompiled with the new behaviour. `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.
* `: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:
* `: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`.
* 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
* `: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 `[]`.
* `: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 set it to `:warn` if you want undefined variables to
emit a warning and expand as to a local call to the zero-arity function
of the same name (for example, `node` would be expanded as `node()`).
This `:warn` behavior only exists for compatibility reasons when working
with old dependencies, its usage is discouraged and it will be removed
in future releases.
* `:parser_options` (since v1.10.0) - a keyword list of options to be given
to the parser when compiling files. It accepts the same options as
@@ -1767,14 +1831,12 @@ 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.
* `: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.
* `: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 `[]`.
It always returns `:ok`. Raises an error for invalid options.
@@ -1814,6 +1876,15 @@ 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
@@ -2079,7 +2150,7 @@ defmodule Code do
If the module being checked is currently in a compiler deadlock,
this function returns `{:error, :unavailable}`. Unavailable doesn't
necessarily mean the module doesn't exist, just that it is not currently
available, but it (or may not) become available in the future.
available, but it may (or may not) become available in the future.
Therefore, if you can only continue if the module is available, use
`ensure_compiled!/1` instead. In particular, do not do this:
+18 -10
View File
@@ -195,6 +195,7 @@ defmodule Code.Formatter do
file = Keyword.get(opts, :file, nil)
sigils = Keyword.get(opts, :sigils, [])
migrate = Keyword.get(opts, :migrate, false)
migrate_atom_interpolations = Keyword.get(opts, :migrate_atom_interpolations, migrate)
migrate_bitstring_modifiers = Keyword.get(opts, :migrate_bitstring_modifiers, migrate)
migrate_call_parens_on_pipe = Keyword.get(opts, :migrate_call_parens_on_pipe, migrate)
migrate_charlists_as_sigils = Keyword.get(opts, :migrate_charlists_as_sigils, migrate)
@@ -223,6 +224,7 @@ defmodule Code.Formatter do
comments: comments,
sigils: sigils,
file: file,
migrate_atom_interpolations: migrate_atom_interpolations,
migrate_bitstring_modifiers: migrate_bitstring_modifiers,
migrate_call_parens_on_pipe: migrate_call_parens_on_pipe,
migrate_charlists_as_sigils: migrate_charlists_as_sigils,
@@ -334,14 +336,20 @@ defmodule Code.Formatter do
end
defp quoted_to_algebra(
{{:., _, [:erlang, :binary_to_atom]}, _, [{:<<>>, _, entries}, :utf8]} = quoted,
{{:., _, [:erlang, :binary_to_atom]}, _, [{:<<>>, _, entries} = bitstring, :utf8]} =
quoted,
context,
state
) do
if interpolated?(entries) do
interpolation_to_algebra(entries, @double_quote, state, ":\"", @double_quote)
else
remote_to_algebra(quoted, context, state)
cond do
not interpolated?(entries) ->
remote_to_algebra(quoted, context, state)
state.migrate_atom_interpolations ->
quoted_to_algebra(quote(do: String.to_unsafe_atom(unquote(bitstring))), context, state)
true ->
interpolation_to_algebra(entries, @double_quote, state, ":\"", @double_quote)
end
end
@@ -1457,7 +1465,7 @@ defmodule Code.Formatter do
metadata = [
file: state.file,
line: meta[:line],
sigil: String.to_atom(name),
sigil: String.to_unsafe_atom(name),
modifiers: modifiers,
opening_delimiter: opening_delimiter
]
@@ -1696,7 +1704,7 @@ defmodule Code.Formatter do
iodata |> IO.iodata_to_binary() |> string() |> color_doc(:atom, inspect_opts)
end
defp integer_to_algebra(text, inspect_otps) do
defp integer_to_algebra(text, inspect_opts) do
case text do
<<?0, ?x, rest::binary>> ->
"0x" <> String.upcase(rest)
@@ -1710,15 +1718,15 @@ defmodule Code.Formatter do
decimal ->
insert_underscores(decimal)
end
|> color_doc(:number, inspect_otps)
|> color_doc(:number, inspect_opts)
end
defp float_to_algebra(text, inspect_otps) do
defp float_to_algebra(text, inspect_opts) do
[int_part, decimal_part] = :binary.split(text, ".")
decimal_part = String.downcase(decimal_part)
string = insert_underscores(int_part) <> "." <> decimal_part
color_doc(string, :number, inspect_otps)
color_doc(string, :number, inspect_opts)
end
defp insert_underscores("-" <> digits) do
+51 -11
View File
@@ -27,7 +27,8 @@ defmodule Code.Fragment do
column: pos_integer(),
columns: boolean(),
token_metadata: boolean(),
literal_encoder: (term(), Macro.metadata() -> term()),
literal_encoder: (term(), Macro.metadata() -> {:ok, Macro.t()} | {:error, binary()}),
preserve_sigils: boolean(),
trailing_fragment: String.t()
]
@@ -35,7 +36,7 @@ defmodule Code.Fragment do
Returns the list of lines in the given string, preserving their line endings.
Only the line endings recognized by the Elixir compiler are
considered, namely `\r\n` and `\n`. If you would like the retrieve
considered, namely `\r\n` and `\n`. If you would like to retrieve
lines without their line endings, use `String.split(string, ["\r\n", "\n"])`.
## Examples
@@ -51,6 +52,7 @@ defmodule Code.Fragment do
"""
@doc since: "1.19.0"
@spec lines(String.t()) :: [String.t()]
def lines(string) do
lines(string, <<>>)
end
@@ -140,6 +142,9 @@ defmodule Code.Fragment do
* `{:anonymous_call, inside_caller}` - the context is an anonymous
call, such as `fun.(` and `@fun.(`.
* `{:capture_arg, charlist}` - the context is a capture argument,
such as `&1`
* `{:module_attribute, charlist}` - the context is a module attribute,
such as `@hello_wor`
@@ -157,8 +162,8 @@ defmodule Code.Fragment do
* `:none` - no context possible
* `{:sigil, charlist}` - the context is a sigil. It may be either the beginning
of a sigil, such as `~` or `~s`, or an operator starting with `~`, such as
`~>` and `~>>`
of a sigil, such as `~` or `~s`. Operators starting with `~`, such as
`~>` and `~>>`, are returned as :operator contexts
* `{:struct, inside_struct}` - the context is a struct, such as `%`, `%UR` or `%URI`.
`inside_struct` can either be a `charlist` in case of a static alias or an
@@ -204,6 +209,7 @@ 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}
@@ -656,7 +662,7 @@ defmodule Code.Fragment do
iex> Code.Fragment.surround_context("foo", {1, 1})
%{begin: {1, 1}, context: {:local_or_var, ~c"foo"}, end: {1, 4}}
## Differences to `cursor_context/2`
## Differences from `cursor_context/2`
Because `surround_context/3` attempts to capture complex expressions,
it has some differences to `cursor_context/2`:
@@ -670,7 +676,7 @@ defmodule Code.Fragment do
be a local or variable
* `@` when not followed by any identifier is returned as `{:operator, ~c"@"}`
(in contrast to `{:module_attribute, ~c""}` in `cursor_context/2`
(in contrast to `{:module_attribute, ~c""}` in `cursor_context/2`)
* This function never returns empty sigils `{:sigil, ~c""}` or empty structs
`{:struct, ~c""}` as context
@@ -1234,14 +1240,43 @@ 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, []}, emit_warnings: false] ++ opts
opts = [check_terminators: {:cursor, preserve_sigils?, []}] ++ opts
file = Keyword.get(opts, :file, "nofile")
line = Keyword.get(opts, :line, 1)
@@ -1261,7 +1296,10 @@ defmodule Code.Fragment do
end
tokens = reverse_tokens(line, column, rev_tokens, rev_terminators)
:elixir.tokens_to_quoted(tokens, file, opts)
with {:ok, forms, _warnings} <- :elixir.tokens_to_quoted(tokens, file, opts) do
{:ok, forms}
end
{:ok, line, column, _warnings, rev_tokens, rev_terminators} ->
tokens =
@@ -1269,7 +1307,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, before_start}),
Keyword.put(opts, :check_terminators, {:cursor, false, before_start}),
{:error, {meta, _, ~c"end"}, _rest, _warnings, trailing_rev_tokens} <-
:elixir_tokenizer.tokenize(to_charlist(trailing_fragment), line, column, opts) do
trailing_tokens =
@@ -1288,10 +1326,12 @@ defmodule Code.Fragment do
_ -> reverse_tokens(line, column, rev_tokens, rev_terminators)
end
:elixir.tokens_to_quoted(tokens, file, opts)
with {:ok, forms, _warnings} <- :elixir.tokens_to_quoted(tokens, file, opts) do
{:ok, forms}
end
{:error, info, _rest, _warnings, _so_far} ->
{:error, :elixir.format_token_error(info)}
{:error, :elixir_tokenizer.format_error(info)}
end
end
+4 -4
View File
@@ -65,7 +65,7 @@ defmodule Code.Identifier do
with "-" <> rest <- Atom.to_string(atom),
[trailing | reversed] = rest |> String.split("/") |> Enum.reverse(),
[arity, _inner, _count, ""] <- String.split(trailing, "-") do
{reversed |> Enum.reverse() |> Enum.join("/") |> String.to_atom(), arity}
{reversed |> Enum.reverse() |> Enum.join("/") |> String.to_unsafe_atom(), arity}
else
_ -> :error
end
@@ -150,20 +150,20 @@ defmodule Code.Identifier do
<<acc::binary, char::utf8>>
end
defp escape_char(char, acc) when char < 0x100 do
defp escape_char(char, acc) when char < 0x80 do
<<a::4, b::4>> = <<char::8>>
<<acc::binary, ?\\, ?x, to_hex(a), to_hex(b)>>
end
defp escape_char(char, acc) when char < 0x10000 do
<<a::4, b::4, c::4, d::4>> = <<char::16>>
<<acc::binary, ?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), ?}>>
<<acc::binary, ?\\, ?u, to_hex(a), to_hex(b), to_hex(c), to_hex(d)>>
end
defp escape_char(char, acc) when char < 0x1000000 do
<<a::4, b::4, c::4, d::4, e::4, f::4>> = <<char::24>>
<<acc::binary, ?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), to_hex(e), to_hex(f),
<<acc::binary, ?\\, ?u, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), to_hex(e), to_hex(f),
?}>>
end
+37 -14
View File
@@ -4,6 +4,8 @@
defmodule Code.Normalizer do
@moduledoc false
@do_end_keywords [:rescue, :catch, :else, :after]
defguard is_literal(x)
when is_integer(x) or
is_float(x) or
@@ -68,7 +70,7 @@ defmodule Code.Normalizer do
# Bit containers
defp do_normalize({:<<>>, _, args} = quoted, state) when is_list(args) do
normalize_bitstring(quoted, state)
normalize_bitstring(quoted, state, state.escape)
end
# Atoms with interpolations
@@ -89,13 +91,7 @@ defmodule Code.Normalizer do
normalize_literal(:utf8, [], state)
end
string =
if state.escape do
normalize_bitstring(string, state, true)
else
normalize_bitstring(string, state)
end
string = normalize_bitstring(string, state, state.escape)
{{:., dot_meta, [:erlang, :binary_to_atom]}, call_meta, [string, utf8]}
end
@@ -118,6 +114,7 @@ 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)
@@ -187,7 +184,8 @@ defmodule Code.Normalizer do
|> patch_meta_line(state.parent_meta)
|> Keyword.put_new(:delimiter, "\"")
{sigil, meta, [do_normalize(string, %{state | parent_meta: meta}), modifiers]}
string = normalize_bitstring(string, %{state | parent_meta: meta}, false)
{sigil, meta, [string, modifiers]}
else
_ ->
normalize_call(quoted, state)
@@ -269,7 +267,7 @@ defmodule Code.Normalizer do
"Elixir." <> segments ->
segments
|> String.split(".")
|> Enum.map(&String.to_atom/1)
|> Enum.map(&String.to_unsafe_atom/1)
end
{:__aliases__, meta, segments}
@@ -351,18 +349,20 @@ defmodule Code.Normalizer do
args = normalize_args(args, %{state | parent_meta: meta})
{form, meta, args}
Keyword.has_key?(meta, :do) ->
Keyword.has_key?(meta, :do) and kw_blocks?(last) ->
# def foo do :ok end
# def foo, do: :ok
normalize_kw_blocks(form, meta, args, state)
match?([{:do, _} | _], last) and Keyword.keyword?(last) ->
match?([{:do, _} | _], last) and kw_blocks?(last) ->
# Non normalized kw blocks
line = state.parent_meta[:line] || meta[:line]
meta = meta ++ [do: [line: line], end: [line: line]]
normalize_kw_blocks(form, meta, args, state)
true ->
# The formatter renders do-end blocks from the meta alone
meta = Keyword.drop(meta, [:do, :end])
args = normalize_args(args, %{state | parent_meta: meta})
{last_arg, leading_args} = List.pop_at(args, -1, [])
@@ -401,11 +401,22 @@ defmodule Code.Normalizer do
defp block_keyword?([]), do: true
defp block_keyword?(_), do: false
# Anything after the do block that is not a block keyword makes it a keyword list
defp kw_blocks?([{:do, _} | rest] = kw) do
Keyword.keyword?(kw) and Enum.all?(rest, &match?({key, _} when key in @do_end_keywords, &1))
end
defp kw_blocks?([{{:__block__, _, [:do]}, _} | rest]) do
Enum.all?(rest, &match?({{:__block__, _, [key]}, _} when key in @do_end_keywords, &1))
end
defp kw_blocks?(_), do: false
defp allow_keyword?(:when, 2), do: true
defp allow_keyword?(:{}, _), 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 \\ false) do
defp normalize_bitstring({:<<>>, meta, parts}, state, escape_interpolation) do
meta = patch_meta_line(meta, state.parent_meta)
parts =
@@ -424,9 +435,21 @@ defmodule Code.Normalizer do
end)
end
parts = maybe_add_trailing_newline(meta, parts, state)
{:<<>>, 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,
@@ -552,7 +575,7 @@ defmodule Code.Normalizer do
atom
|> Atom.to_string()
|> maybe_escape_literal(state)
|> String.to_atom()
|> String.to_unsafe_atom()
end
defp maybe_escape_literal(term, _) do
+11 -9
View File
@@ -80,7 +80,8 @@ defmodule Code.Typespec do
Returns all types available from the module's BEAM code.
The result is returned as a list of tuples where the first
element is the type (`:typep`, `:type` and `:opaque`).
element is the type (`:typep`, `:type`, `:opaque` and, on Erlang/OTP 28+,
`:nominal`).
The module must have a corresponding BEAM file which can be
located by the runtime system. The types will be in the Erlang
@@ -95,9 +96,10 @@ defmodule Code.Typespec do
types =
for {:attribute, _, kind, {name, _, args} = type} <- abstract_code,
kind in [:opaque, :type] do
kind in [:opaque, :type, :nominal] do
cond do
kind == :opaque -> {:opaque, type}
kind == :nominal -> {:nominal, type}
{name, length(args)} in exported_types -> {:type, type}
true -> {:typep, type}
end
@@ -117,7 +119,7 @@ defmodule Code.Typespec do
element is spec name and arity and the second is the spec.
The module must have a corresponding BEAM file which can be
located by the runtime system. The types will be in the Erlang
located by the runtime system. The specs will be in the Erlang
Abstract Format.
"""
@spec fetch_specs(module | binary) :: {:ok, [tuple]} | :error
@@ -135,10 +137,10 @@ defmodule Code.Typespec do
Returns all callbacks available from the module's BEAM code.
The result is returned as a list of tuples where the first
element is spec name and arity and the second is the spec.
element is the callback name and arity and the second is the callback.
The module must have a corresponding BEAM file
which can be located by the runtime system. The types will be
which can be located by the runtime system. The callbacks will be
in the Erlang Abstract Format.
"""
@spec fetch_callbacks(module | binary) :: {:ok, [tuple]} | :error
@@ -191,8 +193,8 @@ defmodule Code.Typespec do
## To AST conversion
defp collect_vars({:ann_type, _anno, args}) when is_list(args) do
[]
defp collect_vars({:ann_type, _anno, [_var, type]}) do
collect_vars(type)
end
defp collect_vars({:type, _anno, _kind, args}) when is_list(args) do
@@ -399,10 +401,10 @@ defmodule Code.Typespec do
defp erl_to_ex_var(var) do
case Atom.to_string(var) do
<<"_", c::utf8, rest::binary>> ->
String.to_atom("_#{String.downcase(<<c::utf8>>)}#{rest}")
String.to_unsafe_atom("_#{String.downcase(<<c::utf8>>)}#{rest}")
<<c::utf8, rest::binary>> ->
String.to_atom("#{String.downcase(<<c::utf8>>)}#{rest}")
String.to_unsafe_atom("#{String.downcase(<<c::utf8>>)}#{rest}")
end
end
+16
View File
@@ -69,6 +69,22 @@ 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
+19 -10
View File
@@ -152,6 +152,7 @@ defmodule Config do
"""
@doc since: "1.9.0"
@spec config(atom(), keyword()) :: keyword()
def config(root_key, opts) when is_atom(root_key) and is_list(opts) do
if not Keyword.keyword?(opts) do
raise ArgumentError, "config/2 expected a keyword list, got: #{inspect(opts)}"
@@ -198,6 +199,7 @@ defmodule Config do
"""
@doc since: "1.9.0"
@spec config(atom(), atom(), term()) :: keyword()
def config(root_key, key, opts) when is_atom(root_key) and is_atom(key) do
get_config!()
|> __merge__([{root_key, [{key, opts}]}])
@@ -225,6 +227,7 @@ defmodule Config do
"""
@doc since: "1.18.0"
@spec read_config(atom()) :: keyword() | nil
def read_config(root_key) when is_atom(root_key) do
get_config!()[root_key]
end
@@ -233,7 +236,7 @@ defmodule Config do
Returns the environment this configuration file is executed on.
In Mix projects this function returns the environment this configuration
file is executed on.
file is executed on.
In releases, returns the `MIX_ENV` specified when running `mix release`.
This is most often used to execute conditional code:
@@ -284,8 +287,8 @@ defmodule Config do
In case the file doesn't exist, an error is raised.
If file is a relative, it will be expanded relatively to the
directory the current configuration file is in.
If the file is relative, it will be expanded relative to the
directory of the current configuration file.
## Examples
@@ -377,21 +380,27 @@ defmodule Config do
end
end
defp validate!(config, file) do
Enum.all?(config, fn
defp validate!(config, file) when is_list(config) do
Enum.each(config, fn
{app, value} when is_atom(app) ->
if Keyword.keyword?(value) do
true
else
if not Keyword.keyword?(value) do
raise ArgumentError,
"expected config for app #{inspect(app)} in #{Path.relative_to_cwd(file)} " <>
"to return keyword list, got: #{inspect(value)}"
end
_ ->
false
other ->
raise ArgumentError,
"expected config in #{Path.relative_to_cwd(file)} to be a keyword list " <>
"of {atom, keyword} pairs, got entry: #{inspect(other)}"
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
+1 -1
View File
@@ -25,7 +25,7 @@ defmodule Config.Provider do
For example, imagine you want to list some basic configuration
on Mix's built-in `config/runtime.exs` file, but you also want
to support additional configuration files. To do so, you can add
this inside the `def project` portion of your `mix.exs`:
this inside the `def project` portion of your `mix.exs`:
releases: [
demo: [
+1 -1
View File
@@ -16,7 +16,7 @@ defmodule Config.Reader do
For example, if you expect the target system to have a config file
in an absolute path, you can add this inside the `def project` portion
of your `mix.exs`:
of your `mix.exs`:
releases: [
demo: [
+109 -57
View File
@@ -618,7 +618,7 @@ defmodule Enum do
acc,
(element, acc -> {:cont, chunk, acc} | {:cont, acc} | {:halt, acc}),
(acc -> {:cont, chunk, acc} | {:cont, acc})
) :: Enumerable.t()
) :: [chunk]
when chunk: any
def chunk_while(enumerable, acc, chunk_fun, after_fun) do
{_, {res, acc}} =
@@ -666,7 +666,7 @@ defmodule Enum do
[1, [2], 3, 4, 5, 6]
"""
@spec concat(t) :: t
@spec concat(Enumerable.t(Enumerable.t(elem))) :: [elem] when elem: term
def concat(enumerables)
def concat(list) when is_list(list) do
@@ -681,8 +681,8 @@ defmodule Enum do
Concatenates the enumerable on the `right` with the enumerable on the
`left`.
This function produces the same result as the `++/2` operator
for lists.
This function behaves similarly to the `++/2` operator with proper
lists, but applied to enumerables.
## Examples
@@ -693,7 +693,7 @@ defmodule Enum do
[1, 2, 3, 4, 5, 6]
"""
@spec concat(t, t) :: t
@spec concat(Enumerable.t(elem), Enumerable.t(elem)) :: [elem] when elem: term
def concat(left, right) when is_list(left) and is_list(right) do
left ++ right
end
@@ -829,7 +829,7 @@ defmodule Enum do
"""
@spec dedup(t) :: list
def dedup(enumerable) when is_list(enumerable) do
dedup_list(enumerable, []) |> :lists.reverse()
dedup_list(enumerable)
end
def dedup(enumerable) do
@@ -859,6 +859,10 @@ defmodule Enum do
"""
@spec dedup_by(t, (element -> term)) :: list
def dedup_by([head | tail], fun) do
dedup_by_list(tail, fun, fun.(head), [head])
end
def dedup_by(enumerable, fun) do
{list, _} = reduce(enumerable, {[], []}, R.dedup(fun))
:lists.reverse(list)
@@ -903,7 +907,7 @@ defmodule Enum do
def drop(enumerable, amount) when is_integer(amount) and amount < 0 do
{count, fun} = slice_count_and_fun(enumerable, 1)
amount = Kernel.min(amount + count, count)
amount = amount + count
if amount > 0 do
fun.(0, amount, 1)
@@ -1253,7 +1257,7 @@ defmodule Enum do
iex> Enum.flat_map([:a, :b, :c], fn x -> [[x]] end)
[[:a], [:b], [:c]]
This is frequently used to to transform and filter in one pass, returning empty
This is frequently used to transform and filter in one pass, returning empty
lists to exclude results:
iex> Enum.flat_map([4, 0, 2, 0], fn x ->
@@ -1442,7 +1446,7 @@ defmodule Enum do
)
# Avoid warnings about Dict
dict_module = String.to_atom("Dict")
dict_module = String.to_unsafe_atom("Dict")
reduce(reverse(enumerable), dict, fn entry, categories ->
dict_module.update(categories, fun.(entry), [entry], &[entry | &1])
@@ -2023,11 +2027,16 @@ defmodule Enum do
operators work by using this function.
"""
@spec member?(t, element) :: boolean
def member?(enumerable, element) when is_list(enumerable) do
def member?(enumerable, element) do
__in__(element, enumerable)
end
@doc false
def __in__(element, enumerable) when is_list(enumerable) do
:lists.member(element, enumerable)
end
def member?(enumerable, element) do
def __in__(element, enumerable) do
case Enumerable.member?(enumerable, element) do
{:ok, element} when is_boolean(element) ->
element
@@ -2224,14 +2233,19 @@ defmodule Enum do
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()) :: {min :: element, max :: element}
@spec min_max(t, (-> empty_result)) :: {min :: element, max :: element} | empty_result
when empty_result: any
@spec min_max(t, (element, element -> boolean) | module(), (-> empty_result)) ::
{element, element} | empty_result
{min :: element, max :: element} | empty_result
when empty_result: any
def min_max(enumerable, sorter_or_empty_fallback \\ fn -> raise Enum.EmptyError end)
def min_max(list = [_ | _], empty_fallback) when is_function(empty_fallback, 0) do
min_max_list(list)
end
def min_max(first..last//step = range, empty_fallback)
when is_function(empty_fallback, 0) do
case Range.size(range) do
@@ -2338,14 +2352,14 @@ defmodule Enum do
"""
@spec min_max_by(t, (element -> any), (element, element -> boolean) | module()) ::
{element, element} | empty_result
{min :: element, max :: element} | empty_result
when empty_result: any
@spec min_max_by(
t,
(element -> any),
(element, element -> boolean) | module(),
(-> empty_result)
) :: {element, element} | empty_result
) :: {min :: element, max :: element} | empty_result
when empty_result: any
def min_max_by(
enumerable,
@@ -2389,6 +2403,18 @@ defmodule Enum do
defp min_max_sort_fun(module) when is_atom(module), do: &(module.compare(&1, &2) == :lt)
defp min_max_list([h | t]), do: min_max_list(t, h, h)
defp min_max_list([h | t], min, max) do
cond do
h < min -> min_max_list(t, h, max)
max < h -> min_max_list(t, min, h)
true -> min_max_list(t, min, max)
end
end
defp min_max_list([], min, max), do: {min, max}
@doc """
Splits the `enumerable` in two lists according to the given function `fun`.
@@ -2666,7 +2692,7 @@ defmodule Enum do
5050
"""
@spec reduce_while(t, any, (element, any -> {:cont, any} | {:halt, any})) :: any
@spec reduce_while(t, acc, (element, acc -> {:cont, acc} | {:halt, acc})) :: acc
def reduce_while(enumerable, acc, fun) do
Enumerable.reduce(enumerable, {:cont, acc}, fun) |> elem(1)
end
@@ -2909,8 +2935,7 @@ defmodule Enum do
end
defp slide_list_middle(list, 0, last, start_to_middle) do
{slid_range, tail} = slide_list_last(list, last + 1, [])
slid_range ++ :lists.reverse(start_to_middle, tail)
slide_list_last(list, last + 1, [], start_to_middle)
end
# You asked for a middle index off the end of the list... you get what we've got
@@ -2918,16 +2943,16 @@ defmodule Enum do
:lists.reverse(acc)
end
defp slide_list_last([h | t], last, acc) when last > 0 do
slide_list_last(t, last - 1, [h | acc])
defp slide_list_last([h | t], last, acc, start_to_middle) when last > 0 do
slide_list_last(t, last - 1, [h | acc], start_to_middle)
end
defp slide_list_last(rest, 0, acc) do
{:lists.reverse(acc), rest}
defp slide_list_last(rest, 0, acc, start_to_middle) do
:lists.reverse(acc, :lists.reverse(start_to_middle, rest))
end
defp slide_list_last([], _, acc) do
{:lists.reverse(acc), []}
defp slide_list_last([], _, acc, start_to_middle) do
:lists.reverse(acc, :lists.reverse(start_to_middle))
end
@doc """
@@ -4193,7 +4218,7 @@ defmodule Enum do
`zip_with/2` can be used to transpose lists of lists:
iex> Enum.zip_with([[1, 2,], [3, 4]], & &1)
iex> Enum.zip_with([[1, 2], [3, 4]], & &1)
[[1, 3], [2, 4]]
"""
@@ -4304,11 +4329,24 @@ defmodule Enum do
empty.()
_ ->
last = last - rem(last - first, step)
# The endpoint shortcut is only valid for sorters consistent with
# the natural integer order of the range elements, which is known
# to hold for the default sorters; any other sorter traverses the
# elements, seeded with the first one since the range is not empty
if fun == (&<=/2) or fun == (&>=/2) do
last = last - rem(last - first, step)
case fun.(first, last) do
true -> first
false -> last
case fun.(first, last) do
true -> first
false -> last
end
else
reduce_range(first + step, last, step, first, fn element, acc ->
case fun.(acc, element) do
true -> acc
false -> element
end
end)
end
end
end
@@ -4411,10 +4449,9 @@ defmodule Enum do
## any?/2 all?/2
defp predicate_list([h | t], initial, fun) do
if !!fun.(h) == initial do
predicate_list(t, initial, fun)
else
not initial
case !!fun.(h) do
^initial -> predicate_list(t, initial, fun)
_ -> not initial
end
end
@@ -4425,10 +4462,9 @@ defmodule Enum do
defp predicate_range(first, last, step, initial, fun)
when step > 0 and first <= last
when step < 0 and first >= last do
if !!fun.(first) == initial do
predicate_range(first + step, last, step, initial, fun)
else
not initial
case !!fun.(first) do
^initial -> predicate_range(first + step, last, step, initial, fun)
_ -> not initial
end
end
@@ -4507,19 +4543,20 @@ defmodule Enum do
# dedup
defp dedup_list([value | tail], acc) do
acc =
case acc do
[^value | _] -> acc
_ -> [value | acc]
end
defp dedup_list([value | [value | _] = tail]), do: dedup_list(tail)
defp dedup_list([value | tail]), do: [value | dedup_list(tail)]
defp dedup_list([]), do: []
dedup_list(tail, acc)
## dedup_by
defp dedup_by_list([head | tail], fun, prev, acc) do
case fun.(head) do
^prev -> dedup_by_list(tail, fun, prev, acc)
new_val -> dedup_by_list(tail, fun, new_val, [head | acc])
end
end
defp dedup_list([], acc) do
acc
end
defp dedup_by_list([], _fun, _prev, acc), do: :lists.reverse(acc)
## drop
@@ -5168,7 +5205,16 @@ defimpl Enumerable, for: Range do
end
# TODO: Remove me on v2.0
def reduce(%{__struct__: Range, first: first, last: last} = range, acc, fun) do
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
step = if first <= last, do: 1, else: -1
reduce(Map.put(range, :step, step), acc, fun)
end
@@ -5191,12 +5237,12 @@ defimpl Enumerable, for: Range do
{:done, acc}
end
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
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}
end
# TODO: Remove me on v2.0
@@ -5215,11 +5261,17 @@ defimpl Enumerable, for: Range do
end
def slice(first.._//step = range) do
{:ok, Range.size(range), &slice(first + &1 * step, step + &3 - 1, &2)}
{:ok, Range.size(range), &slice(first + &1 * step, step * &3, &2)}
end
# TODO: Remove me on v2.0
def slice(%{__struct__: Range, first: first, last: last} = range) do
slice =
quote generated: true do
slice(%{__struct__: Range, first: var!(first), last: var!(last)} = var!(range))
end
def unquote(slice) do
step = if first <= last, do: 1, else: -1
slice(Map.put(range, :step, step))
end
+10 -10
View File
@@ -26,7 +26,7 @@ defmodule Exception do
@typedoc "The exception type"
@type t :: %{
required(:__struct__) => module,
required(:__exception__) => true,
required(:__exception__) => term,
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__: true}), do: true
def exception?(%_{__exception__: _}), 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__: true} = exception) do
def message(%module{__exception__: _} = 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__: true} = payload, _stacktrace), do: payload
def normalize(:error, %_{__exception__: _} = payload, _stacktrace), do: payload
def normalize(:error, payload, stacktrace), do: ErlangError.normalize(payload, stacktrace)
def normalize(_kind, payload, _stacktrace), do: payload
@@ -1046,7 +1046,7 @@ defmodule RuntimeError do
iex> raise "oops!"
** (RuntimeError) oops!
You should use this exceptions sparingly, since most of the time it might be
You should use this exception sparingly, since most of the time it might be
better to define your own exceptions specific to your application or library.
Sometimes, however, there are situations in which you don't expect a condition to
happen, but you want to give a meaningful error message if it does. In those cases,
@@ -1137,7 +1137,7 @@ defmodule SystemLimitError do
For example, this can happen if you try to create an atom that is too large:
iex> String.to_atom(String.duplicate("a", 100_000))
iex> String.to_unsafe_atom(String.duplicate("a", 100_000))
** (SystemLimitError) a system limit has been reached
"""
@@ -1807,7 +1807,7 @@ defmodule UndefinedFunctionError do
defp load_module({name, _path, _loaded?}) do
name
|> List.to_atom()
|> List.to_unsafe_atom()
|> Code.ensure_loaded()
end
@@ -2376,7 +2376,7 @@ defmodule File.Error do
@moduledoc """
An exception that is raised when a file operation fails.
For example, this exception is raised, when trying to read a non existent file:
For example, this exception is raised, when trying to read a nonexistent file:
iex> File.read!("nonexistent_file.txt")
** (File.Error) could not read file "nonexistent_file.txt": no such file or directory
@@ -2409,7 +2409,7 @@ defmodule File.CopyError do
@moduledoc """
An exception that is raised when copying a file fails.
For example, this exception is raised when trying to copy to file or directory that isn't present:
For example, this exception is raised when trying to copy to a file or directory that isn't present:
iex> File.cp_r!("non_existent", "source_dir/subdir")
** (File.CopyError) could not copy recursively from "non_existent" to "source_dir/subdir". non_existent: no such file or directory
@@ -2477,7 +2477,7 @@ defmodule File.LinkError do
@moduledoc """
An exception that is raised when linking a file fails.
For example, this exception is raised when trying to link to file that isn't present:
For example, this exception is raised when trying to link to a file that isn't present:
iex> File.ln!("existing.txt", "link.txt")
** (File.LinkError) could not create hard link from "link.txt" to "existing.txt": no such file or directory
+110 -26
View File
@@ -400,6 +400,13 @@ 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")
@@ -408,15 +415,24 @@ defmodule File do
File.read("non_existing.txt")
#=> {:error, :enoent}
"""
@spec read(Path.t()) :: {:ok, binary} | {:error, posix | :badarg | :terminated | :system_limit}
def read(path) do
:file.read_file(IO.chardata_to_string(path))
@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)
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")
@@ -425,9 +441,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()) :: binary
def read!(path) do
case read(path) do
@spec read!(Path.t(), [exists_option]) :: binary when exists_option: :raw
def read!(path, opts \\ []) do
case read(path, opts) do
{:ok, binary} ->
binary
@@ -881,7 +897,7 @@ defmodule File do
File.copy("non_existing.txt", "copy.txt")
#=> {:error, :enoent}
"""
@spec copy(Path.t() | io_device, Path.t() | io_device, pos_integer | :infinity) ::
@spec copy(Path.t() | io_device, Path.t() | io_device, non_neg_integer | :infinity) ::
{:ok, non_neg_integer} | {:error, posix | :badarg | :terminated}
def copy(source, destination, bytes_count \\ :infinity) do
source = normalize_path_or_io_device(source)
@@ -902,7 +918,7 @@ defmodule File do
File.copy!("non_existing.txt", "copy.txt")
** (File.CopyError) could not copy from "non_existing.txt" to "copy.txt": no such file or directory
"""
@spec copy!(Path.t() | io_device, Path.t() | io_device, pos_integer | :infinity) ::
@spec copy!(Path.t() | io_device, Path.t() | io_device, non_neg_integer | :infinity) ::
non_neg_integer
def copy!(source, destination, bytes_count \\ :infinity) do
case copy(source, destination, bytes_count) do
@@ -1087,7 +1103,7 @@ defmodule File do
@doc ~S"""
Copies the contents in `source` to `destination` recursively, maintaining the
source directory structure and modes.
source directory structure and regular file 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.
@@ -1098,7 +1114,9 @@ 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.
If a file already exists in the destination, it invokes the optional `on_conflict`
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`
callback given as an option. See "Options" for more information.
This function may fail while copying files, in such cases, it will leave the
@@ -1116,6 +1134,12 @@ defmodule File do
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.
@@ -1129,6 +1153,11 @@ 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"
@@ -1146,11 +1175,16 @@ defmodule File do
#=> {:ok, ["z.txt", "y.txt", "x.txt]}
File.cp_r("non_existing.txt", "copy.txt")
#=> {:error, :enoent}
#=> {:error, :enoent, "non_existing.txt"}
# Copying into a subdirectory of source is not allowed
File.cp_r("src", "src/dest")
#=> {:error, :einval, "src/dest"}
"""
@spec cp_r(Path.t(), Path.t(),
on_conflict: on_conflict_callback,
dereference_symlinks: boolean()
dereference_symlinks: boolean(),
preserve_directory_permissions: boolean()
) ::
{:ok, [binary]} | {:error, posix | :badarg | :terminated, binary}
@@ -1172,6 +1206,7 @@ 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
@@ -1183,9 +1218,25 @@ defmodule File do
|> IO.chardata_to_string()
|> assert_no_null_byte!("File.cp_r/3")
case do_cp_r(source, destination, on_conflict, dereference?, []) do
{:error, _, _} = error -> error
res -> {:ok, res}
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
end
end
@@ -1206,7 +1257,8 @@ defmodule File do
"""
@spec cp_r!(Path.t(), Path.t(),
on_conflict: on_conflict_callback,
dereference_symlinks: boolean()
dereference_symlinks: boolean(),
preserve_directory_permissions: boolean()
) :: [binary]
def cp_r!(source, destination, options \\ []) do
case cp_r(source, destination, options) do
@@ -1223,7 +1275,7 @@ defmodule File do
end
end
defp do_cp_r(src, dest, on_conflict, dereference?, acc) when is_list(acc) do
defp do_cp_r(src, dest, on_conflict, dereference, preserve_dir_perms?, 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
@@ -1236,8 +1288,15 @@ defmodule File do
{:ok, :symlink} ->
case :file.read_link(src) do
{:ok, link} when dereference? ->
do_cp_r(Path.expand(link, Path.dirname(src)), dest, on_conflict, dereference?, acc)
{: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} ->
do_cp_link(link, src, dest, on_conflict, acc)
@@ -1251,9 +1310,35 @@ defmodule File do
{:ok, files} ->
case mkdir(dest) do
success when success in [:ok, {:error, :eexist}] ->
Enum.reduce(files, [dest | acc], fn x, acc ->
do_cp_r(Path.join(src, x), Path.join(dest, x), on_conflict, dereference?, acc)
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
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}
@@ -1272,14 +1357,13 @@ 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
defp copy_file_mode(src, dest) do
with {:ok, dest_fileinfo} <- stat(dest),
{:ok, src_fileinfo} <- stat(src) do
write_stat(dest, %{dest_fileinfo | mode: src_fileinfo.mode})
with {:ok, src_fileinfo} <- stat(src) do
chmod(dest, src_fileinfo.mode)
end
end
@@ -2136,7 +2220,7 @@ defmodule File do
type. If you pass, for example, `[encoding: :utf8]` or
`[encoding: {:utf16, :little}]` in the modes parameter, the underlying stream
will use `IO.write/2` and the `String.Chars` protocol to convert the data.
See `IO.binwrite/2` and `IO.write/2` .
See `IO.binwrite/2` and `IO.write/2`.
One may also consider passing the `:delayed_write` option if the stream
is meant to be written to under a tight loop.
+20 -7
View File
@@ -18,7 +18,13 @@ defmodule File.Stream do
defstruct path: nil, modes: [], line_or_bytes: :line, raw: true, node: nil
@type t :: %__MODULE__{}
@type t :: %__MODULE__{
path: Path.t(),
modes: [term()],
line_or_bytes: :line | pos_integer(),
raw: boolean(),
node: node()
}
@doc false
def __build__(path, line_or_bytes, modes) do
@@ -119,7 +125,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)
count_lines(device, path, pattern, read_function(stream), 0, :empty)
end
{:ok, open!(stream, modes, counter)}
@@ -229,21 +235,28 @@ 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) do
defp count_lines(device, path, pattern, read, count, last_byte) 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 + count_lines(data, pattern))
count_lines(device, path, pattern, read, count, last_byte)
:eof ->
count
case last_byte do
:empty -> 0
?\n -> count
_ -> count + 1
end
{: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
+190 -141
View File
@@ -25,7 +25,7 @@ defmodule Float do
and arithmetic due to the fact most decimal fractions cannot be
represented by a floating-point binary and most operations are not exact,
but operate on approximations. Those issues are not specific
to Elixir, they are a property of floating point representation itself.
to Elixir, they are a property of floating-point representation itself.
For example, the numbers 0.1 and 0.01 are two of them, what means the result
of squaring 0.1 does not give 0.01 neither the closest representable. Here is
@@ -167,51 +167,73 @@ defmodule Float do
parse_unsigned(binary)
end
defp parse_unsigned(<<digit, rest::binary>>) when digit in ?0..?9,
do: parse_unsigned(rest, false, false, [digit])
defp parse_unsigned(<<digit, rest::binary>> = binary) when digit in ?0..?9,
do: parse_mantissa(binary, rest, false)
defp parse_unsigned(binary) when is_binary(binary), do: :error
defp parse_unsigned(<<digit, rest::binary>>, dot?, e?, acc) when digit in ?0..?9,
do: parse_unsigned(rest, dot?, e?, [digit | acc])
defp parse_mantissa(binary, <<digit, rest::binary>>, dot?) when digit in ?0..?9,
do: parse_mantissa(binary, rest, dot?)
defp parse_unsigned(<<?., digit, rest::binary>>, false, false, acc) when digit in ?0..?9,
do: parse_unsigned(rest, true, false, [digit, ?. | acc])
defp parse_mantissa(binary, <<?., digit, rest::binary>>, false) when digit in ?0..?9,
do: parse_mantissa(binary, rest, true)
defp parse_unsigned(<<exp_marker, digit, rest::binary>>, dot?, false, acc)
defp parse_mantissa(binary, <<exp_marker, digit, rest::binary>> = tail, dot?)
when exp_marker in ~c"eE" and digit in ?0..?9,
do: parse_unsigned(rest, true, true, [digit, ?e | add_dot(acc, dot?)])
do: parse_exponent(binary, byte_size(binary) - byte_size(tail), rest, dot?)
defp parse_unsigned(<<exp_marker, sign, digit, rest::binary>>, dot?, false, acc)
defp parse_mantissa(binary, <<exp_marker, sign, digit, rest::binary>> = tail, dot?)
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?)])
do: parse_exponent(binary, byte_size(binary) - byte_size(tail), rest, dot?)
# When floats are expressed in scientific notation, :erlang.binary_to_float/1 can raise an
# ArgumentError if the e exponent is too big. For example, "1.0e400". Because of this, we
# rescue the ArgumentError here and return an error.
defp parse_unsigned(rest, dot?, true = _e?, acc) do
acc
|> add_dot(dot?)
|> :lists.reverse()
|> :erlang.list_to_float()
defp parse_mantissa(binary, rest, dot?), do: finish_mantissa(binary, rest, dot?)
defp parse_exponent(binary, exp_pos, <<digit, rest::binary>>, dot?) when digit in ?0..?9,
do: parse_exponent(binary, exp_pos, rest, dot?)
defp parse_exponent(binary, exp_pos, rest, dot?),
do: finish_exponent(binary, exp_pos, rest, dot?)
defp finish_mantissa(binary, rest, _dot? = true) do
{:erlang.binary_to_float(consumed(binary, rest)), rest}
rescue
ArgumentError -> :error
else
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}
# Bare integer: * 1.0 casts to the nearest float without building a new binary,
# and raises ArithmeticError on overflow (for example a 400-digit integer).
defp finish_mantissa(binary, rest, _dot? = false) do
{:erlang.binary_to_integer(consumed(binary, rest)) * 1.0, rest}
rescue
ArithmeticError -> :error
end
defp add_dot(acc, true), do: acc
defp add_dot(acc, false), do: [?0, ?. | acc]
# binary_to_float/1 raises ArgumentError when the exponent is too big, e.g. "1.0e400".
defp finish_exponent(binary, _exp_pos, rest, _dot? = true) do
{:erlang.binary_to_float(consumed(binary, rest)), rest}
rescue
ArgumentError -> :error
end
# No decimal point, so ".0" is spliced in before the exponent (at exp_pos) to
# form a valid float literal.
defp finish_exponent(binary, exp_pos, rest, _dot? = false) do
len = byte_size(binary) - byte_size(rest)
literal =
IO.iodata_to_binary([
:binary.part(binary, 0, exp_pos),
".0",
:binary.part(binary, exp_pos, len - exp_pos)
])
{:erlang.binary_to_float(literal), rest}
rescue
ArgumentError -> :error
end
defp consumed(binary, ""), do: binary
defp consumed(binary, rest), do: :binary.part(binary, 0, byte_size(binary) - byte_size(rest))
@doc """
Rounds a float to the largest float less than or equal to `number`.
@@ -264,7 +286,7 @@ defmodule Float do
@doc """
Rounds a float to the smallest float greater than or equal to `number`.
`ceil/2` also accepts a precision to round a floating-point value down
`ceil/2` also accepts a precision to round a floating-point value up
to an arbitrary number of fractional digits (between 0 and 15).
The operation is performed on the binary floating point, without a
@@ -333,7 +355,7 @@ defmodule Float do
and therefore the number above is internally represented as 5.567499999,
which explains the behavior above. If you want exact rounding for decimals,
you must use a decimal library. The behavior above is also in accordance
to reference implementations, such as "Correctly Rounded Binary-Decimal and
with reference implementations, such as "Correctly Rounded Binary-Decimal and
Decimal-Binary Conversions" by David M. Gay.
## Examples
@@ -355,15 +377,12 @@ 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 float === 0.0 or float === -0.0, do: float
def round(float, 0) when is_float(float) do
case float |> :erlang.round() |> :erlang.float() do
case :erlang.round(float) * 1.0 do
zero when zero == 0.0 and float < 0.0 -> -0.0
rounded -> rounded
end
@@ -377,140 +396,170 @@ 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, rounding) do
<<sign::1, exp::11, significant::52-bitstring>> = <<float::float>>
{num, count} = decompose(significant, 1)
count = count - exp + 1023
defp round(float, precision, mode) do
<<sign::1, exp::11, mantissa::52>> = <<float::float>>
cond do
# 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
# 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)
# We are asking more precision than we have
count <= precision ->
# |float| >= 2^52 — has no fractional bits, return unchanged.
exp - 1075 >= 0 ->
float
true ->
# 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
mantissa = @power_of_2_to_52 ||| mantissa
shift = 1075 - exp
do_round(sign, mantissa, shift, precision, mode)
end
end
# 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)
# |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)
end
defp decompose(<<1::1, bits::bitstring>>, count, last_count, acc) do
decompose(bits, count + 1, count, (acc <<< (count - last_count)) + 1)
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(<<0::1, bits::bitstring>>, count, last_count, acc) do
decompose(bits, count + 1, last_count, acc)
end
cond do
rounded_int == 0 ->
signed_zero(sign)
defp decompose(<<>>, _count, last_count, acc) do
{acc, last_count}
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 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)
true ->
bignum_to_float(sign, rounded_int, power)
end
end
defp decimal_to_float(sign, num, den, exp) do
quo = div(num, den)
rem = num - quo * den
defp round_step(:half_up, _sign, quotient, remainder, half) do
if remainder >= half, do: quotient + 1, else: quotient
end
tmp =
case den >>> 1 do
den when rem > den -> quo + 1
den when rem < den -> quo
_ when (quo &&& 1) === 1 -> quo + 1
_ -> quo
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
end
tmp = tmp - @power_of_2_to_52
<<tmp::float>> = <<sign::1, exp + 1023::11, tmp::52>>
tmp
# 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
end
defp rounding(:floor, 1, _num, div), do: div + 1
defp rounding(:ceil, 0, _num, div), do: div + 1
# 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(:half_up, _sign, num, div) do
case rem(num, 10) do
rem when rem < 5 -> div
rem when rem >= 5 -> 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}
end
end
defp rounding(_, _, _, div), do: div
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
Enum.reduce(0..104, 1, fn x, acc ->
defp power_of_10(unquote(x)), do: unquote(acc)
Enum.reduce(0..15, 1, fn exponent, acc ->
defp power_of_10(unquote(exponent)), 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.
@@ -656,7 +705,7 @@ defmodule Float do
end
defp invalid_precision_message(precision) do
"precision #{precision} is out of valid range of #{inspect(@precision_range)}"
"precision #{inspect(precision)} is out of valid range of #{inspect(@precision_range)}"
end
defp expand_compact([{:compact, false} | t]), do: expand_compact(t)
+1 -6
View File
@@ -69,7 +69,6 @@ defmodule Function do
| :name
| :new_index
| :new_uniq
| :pid
| :type
| :uniq
@@ -112,8 +111,6 @@ defmodule Function do
When `fun` is an anonymous function (that is, the type is `:local`), the following
additional keys are returned:
* `:pid` - PID of the process that originally created the function.
* `:index` - (integer) an index into the module function table.
* `:new_index` - (integer) an index into the module function table.
@@ -159,7 +156,7 @@ defmodule Function do
`:module`, `:name`, `:arity`, `:env`, or `:type`.
For anonymous functions, there is also information about any of the
atoms `:index`, `:new_index`, `:new_uniq`, `:uniq`, and `:pid`.
atoms `:index`, `:new_index`, `:new_uniq`, and `:uniq`.
For a named function, the value of any of these items is always the
atom `:undefined`.
@@ -179,8 +176,6 @@ defmodule Function do
iex> fun = &String.length/1
iex> Function.info(fun, :name)
{:name, :length}
iex> Function.info(fun, :pid)
{:pid, :undefined}
"""
@doc since: "1.7.0"
+1 -1
View File
@@ -36,7 +36,7 @@ defmodule GenEvent do
alternative. GenStage is an external Elixir library maintained by the Elixir
team; it provides a tool to implement systems that exchange events in a
demand-driven way with built-in support for back-pressure. See the [GenStage
documentation](https://hexdocs.pm/gen_stage) for more information.
documentation](https://gen-stage.hexdocs.pm) for more information.
### `:gen_event`
+13 -7
View File
@@ -210,13 +210,14 @@ 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
shut down the child, either immediately or by giving it time to shut down,
defaults to `5_000`
For example:
use GenServer, restart: :transient, shutdown: 10_000
See the ["Child specification"](`m:Supervisor#module-child_spec-1-function`) section in the `Supervisor` module for more
See the ["Child specification"](`m:Supervisor#module-child-specification`) 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.
@@ -231,6 +232,8 @@ 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`.
@@ -564,9 +567,12 @@ defmodule GenServer do
`Supervisor`. Likely this approach involves calling `Supervisor.restart_child/2`
after a delay to attempt a restart.
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`.
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.
"""
@callback init(init_arg :: term) ::
{:ok, state}
@@ -857,7 +863,7 @@ defmodule GenServer do
@type on_start :: {:ok, pid} | :ignore | {:error, {:already_started, pid} | term}
@typedoc "The GenServer name"
@type name :: atom | {:global, term} | {:via, module, term}
@type name :: nil | atom | {:global, term} | {:via, module, term}
@typedoc "Options used by the `start*` functions"
@type options :: [option]
@@ -1152,7 +1158,7 @@ defmodule GenServer do
## Timeouts
`timeout` is an integer greater than zero which specifies how many
`timeout` is a non-negative integer which specifies how many
milliseconds to wait for a reply, or the atom `:infinity` to wait
indefinitely. The default value is `5000`. If no reply is received within
the specified time, the function call fails and the caller exits. If the
+5 -5
View File
@@ -265,13 +265,13 @@ defimpl Enumerable, for: HashDict do
def reduce(dict, acc, fun) do
# Avoid warnings about HashDict being deprecated.
module = String.to_atom("HashDict")
module = String.to_unsafe_atom("HashDict")
module.reduce(dict, acc, fun)
end
def member?(dict, {key, value}) do
# Avoid warnings about HashDict being deprecated.
module = String.to_atom("HashDict")
module = String.to_unsafe_atom("HashDict")
{:ok, match?({:ok, ^value}, module.fetch(dict, key))}
end
@@ -281,7 +281,7 @@ defimpl Enumerable, for: HashDict do
def count(dict) do
# Avoid warnings about HashDict being deprecated.
module = String.to_atom("HashDict")
module = String.to_unsafe_atom("HashDict")
{:ok, module.size(dict)}
end
@@ -296,7 +296,7 @@ defimpl Collectable, for: HashDict do
def into(original) do
# Avoid warnings about HashDict being deprecated.
module = String.to_atom("HashDict")
module = String.to_unsafe_atom("HashDict")
collector_fun = fn
dict, {:cont, {key, value}} -> module.put(dict, key, value)
@@ -315,7 +315,7 @@ defimpl Inspect, for: HashDict do
def inspect(dict, opts) do
# Avoid warnings about HashDict being deprecated.
module = String.to_atom("HashDict")
module = String.to_unsafe_atom("HashDict")
concat(["#HashDict<", Inspect.List.inspect(module.to_list(dict), opts), ">"])
end
end
+5 -5
View File
@@ -279,19 +279,19 @@ defimpl Enumerable, for: HashSet do
def reduce(set, acc, fun) do
# Avoid warnings about HashSet being deprecated.
module = String.to_atom("HashSet")
module = String.to_unsafe_atom("HashSet")
module.reduce(set, acc, fun)
end
def member?(set, term) do
# Avoid warnings about HashSet being deprecated.
module = String.to_atom("HashSet")
module = String.to_unsafe_atom("HashSet")
{:ok, module.member?(set, term)}
end
def count(set) do
# Avoid warnings about HashSet being deprecated.
module = String.to_atom("HashSet")
module = String.to_unsafe_atom("HashSet")
{:ok, module.size(set)}
end
@@ -306,7 +306,7 @@ defimpl Collectable, for: HashSet do
def into(original) do
# Avoid warnings about HashSet being deprecated.
module = String.to_atom("HashSet")
module = String.to_unsafe_atom("HashSet")
collector_fun = fn
set, {:cont, term} -> module.put(set, term)
@@ -325,7 +325,7 @@ defimpl Inspect, for: HashSet do
def inspect(set, opts) do
# Avoid warnings about HashSet being deprecated.
module = String.to_atom("HashSet")
module = String.to_unsafe_atom("HashSet")
concat(["#HashSet<", Inspect.List.inspect(module.to_list(set), opts), ">"])
end
end
+42 -9
View File
@@ -92,7 +92,7 @@ defprotocol Inspect do
end
inspect(%Point{x: 1})
%Point{x: 1, y: 0}
#=> %Point{x: 1, y: 0}
## Custom implementation
@@ -387,13 +387,20 @@ defimpl Inspect, for: List do
close = color_doc("]", :list, opts)
cond do
lists == :as_charlists or (lists == :infer and List.ascii_printable?(term, printable_limit)) ->
inspected =
case Identifier.escape(IO.chardata_to_string(term), ?", printable_limit) do
{escaped, ""} -> [?~, ?c, ?", escaped, ?"]
{escaped, _} -> [?~, ?c, ?", escaped, ?", " ++ ..."]
(lists == :as_charlists and unicode_list?(term, printable_limit)) or
(lists == :infer and List.ascii_printable?(term, printable_limit)) ->
{split, tail} =
if is_integer(printable_limit) do
case Enum.split(term, printable_limit) do
{split, []} -> {split, []}
{split, _} -> {split, " ++ ..."}
end
else
{term, []}
end
{escaped, _} = Identifier.escape(IO.chardata_to_string(split), ?")
inspected = [?~, ?c, ?", escaped, ?" | tail]
color_doc(IO.iodata_to_binary(inspected), :charlist, opts)
keyword?(term) ->
@@ -407,6 +414,19 @@ defimpl Inspect, for: List do
end
end
defp unicode_list?(_, 0), do: true
defp unicode_list?([char | rest], counter)
when char in 0..0xD7FF or char in 0xE000..0x10FFFF,
do: unicode_list?(rest, decrement(counter))
defp unicode_list?([], _counter), do: true
defp unicode_list?(_, _counter), do: false
@compile {:inline, decrement: 1}
defp decrement(:infinity), do: :infinity
defp decrement(counter), do: counter - 1
@doc false
def keyword({key, value}, opts) do
key = color_doc(Macro.inspect_atom(:key, key), :atom, opts)
@@ -416,8 +436,8 @@ defimpl Inspect, for: List do
@doc false
def keyword?([{key, _value} | rest]) when is_atom(key) do
case Atom.to_charlist(key) do
[?E, ?l, ?i, ?x, ?i, ?r, ?.] ++ _ -> false
case Atom.to_string(key) do
"Elixir." <> _ -> false
_ -> keyword?(rest)
end
end
@@ -669,6 +689,11 @@ 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)
@@ -696,7 +721,15 @@ defimpl Inspect, for: Range do
end
# TODO: Remove me on v2.0
def inspect(%{__struct__: Range, first: first, last: last} = range, opts) do
inspect =
quote generated: true do
inspect(
%{__struct__: Range, first: var!(first), last: var!(last)} = var!(range),
var!(opts)
)
end
def unquote(inspect) do
step = if first <= last, do: 1, else: -1
inspect(Map.put(range, :step, step), opts)
end
+5 -5
View File
@@ -21,8 +21,8 @@ defmodule Inspect.Opts do
is `:decimal` and if it is printable, otherwise in bit syntax. See
`String.printable?/1` to learn when a string is printable.
* `:charlists` - when `:as_charlists` all lists will be printed as charlists,
non-printable elements will be escaped.
* `:charlists` - when `:as_charlists` all charlists will be printed as charlists,
non-printable code points will be escaped. Other lists will be printed as lists.
When `:as_lists` all lists will be printed as lists.
@@ -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 `100` since
`Elixir v1.19.0`, as it has better defaults to deal with nested collections.
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.
* `: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: 100,
limit: 200,
pretty: false,
printable_limit: 4096,
safe: true,
+55 -6
View File
@@ -18,6 +18,45 @@ 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.
@@ -69,7 +108,7 @@ defmodule Integer do
defguard is_even(integer) when is_integer(integer) and (integer &&& 1) == 0
@doc """
Computes `base` raised to power of `exponent`.
Computes `base` raised to the power of `exponent`.
Both `base` and `exponent` must be integers.
The exponent must be zero or positive.
@@ -258,8 +297,9 @@ defmodule Integer do
defp undigits([], _base, acc), do: acc
defp undigits([digit | _], base, _) when is_integer(digit) and digit >= base,
do: raise(ArgumentError, "invalid digit #{digit} in base #{base}")
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 | tail], base, acc) when is_integer(digit),
do: undigits(tail, base, acc * base + digit)
@@ -270,7 +310,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, remainder_of_binary}`.
If successful, returns a tuple in the form of `{integer, remaining_string}`.
Otherwise `:error`.
Raises an error if `base` is less than 2 or more than 36.
@@ -289,6 +329,9 @@ defmodule Integer do
iex> Integer.parse("three")
:error
iex> Integer.parse("404 not found")
{404, " not found"}
iex> Integer.parse("34", 10)
{34, ""}
@@ -489,8 +532,12 @@ 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}
@@ -498,8 +545,10 @@ 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), do: {b, 0, 1}
def extended_gcd(a, 0), do: {a, 1, 0}
def extended_gcd(0, b) when is_integer(b) and b > 0, do: {b, 0, 1}
def extended_gcd(0, b) when is_integer(b) and b < 0, do: {-b, 0, -1}
def extended_gcd(a, 0) when is_integer(a) and a > 0, do: {a, 1, 0}
def extended_gcd(a, 0) when is_integer(a) and a < 0, do: {-a, -1, 0}
def extended_gcd(integer1, integer2) when is_integer(integer1) and is_integer(integer2) do
extended_gcd(integer2, integer1, 0, 1, 1, 0)
+6 -6
View File
@@ -128,7 +128,7 @@ defmodule IO do
@type nodata :: {:error, term} | :eof
@type chardata :: String.t() | maybe_improper_list(char | chardata, String.t() | [])
@type inspect_opts :: [Inspect.Opts.new_opt() | {:label, term}]
@type inspect_opts :: [Inspect.Opts.new_opt() | {:label, String.Chars.t()}]
@typedoc """
Stacktrace information as keyword options for `warn/2`.
@@ -152,9 +152,9 @@ defmodule IO do
The `device` is iterated as specified by the `line_or_chars` argument:
* if `line_or_chars` is an integer, it represents a number of bytes. The device is
iterated by that number of bytes. This should be the preferred mode for reading
non-textual inputs.
* if `line_or_chars` is an integer, it is the number of Unicode
code points to be retrieved for devices open in Unicode/utf8 mode.
Otherwise, it is the number of raw bytes to be retrieved.
* if `line_or_chars` is `:line`, the device is iterated line by line.
CRLF newlines ("\r\n") are automatically normalized to "\n".
@@ -521,7 +521,7 @@ defmodule IO do
end
@doc """
Gets a number of bytes from IO device `:stdio`.
Gets a number of characters from IO device `:stdio`.
If `:stdio` is a Unicode device, `count` implies
the number of Unicode code points to be retrieved.
@@ -549,7 +549,7 @@ defmodule IO do
end
@doc """
Gets a number of bytes from the IO `device`.
Gets a number of characters from the IO `device`.
If the IO `device` is a Unicode device, `count` implies
the number of Unicode code points to be retrieved.
+9 -7
View File
@@ -111,13 +111,15 @@ defmodule IO.ANSI do
end
defsequence = fn name, code, terminator ->
sequence = "\e[#{code}#{terminator}"
@spec unquote(name)() :: String.t()
def unquote(name)() do
"\e[#{unquote(code)}#{unquote(terminator)}"
unquote(sequence)
end
defp format_sequence(unquote(name)) do
unquote(name)()
unquote(sequence)
end
end
@@ -159,7 +161,7 @@ defmodule IO.ANSI do
for font_n <- [1, 2, 3, 4, 5, 6, 7, 8, 9] do
@doc "Sets alternative font #{font_n}."
defsequence.(:"font_#{font_n}", font_n + 10, "m")
defsequence.(String.to_unsafe_atom("font_#{font_n}"), font_n + 10, "m")
end
@doc "Normal color or intensity."
@@ -193,13 +195,13 @@ defmodule IO.ANSI do
defsequence.(color, code + 30, "m")
@doc "Sets foreground color to light #{color}."
defsequence.(:"light_#{color}", code + 90, "m")
defsequence.(String.to_unsafe_atom("light_#{color}"), code + 90, "m")
@doc "Sets background color to #{color}."
defsequence.(:"#{color}_background", code + 40, "m")
defsequence.(String.to_unsafe_atom("#{color}_background"), code + 40, "m")
@doc "Sets background color to light #{color}."
defsequence.(:"light_#{color}_background", code + 100, "m")
defsequence.(String.to_unsafe_atom("light_#{color}_background"), code + 100, "m")
end
@doc "Default text color."
@@ -332,7 +334,7 @@ defmodule IO.ANSI do
end
defp do_format([], [], acc, true, true) do
[acc | IO.ANSI.reset()]
[acc | reset()]
end
defp do_format([], [], acc, _emit?, _append_reset) do
+1 -1
View File
@@ -673,7 +673,7 @@ defmodule IO.ANSI.Docs do
# Characters that can mark the beginning or the end of a word.
# Only support the most common ones at this moment.
@delimiters [?\s, ?', ?", ?!, ?@, ?#, ?$, ?%, ?^, ?&] ++
@delimiters [?\s, ?', ?", ?!, ??, ?,, ?:, ?;, ?/, ?@, ?#, ?$, ?%, ?^, ?&] ++
[?-, ?+, ?(, ?), ?[, ?], ?{, ?}, ?<, ?>, ?.]
### Inline start
+1 -1
View File
@@ -31,7 +31,7 @@ defmodule IO.Stream do
@type t :: %__MODULE__{
device: IO.device(),
raw: boolean(),
line_or_bytes: :line | non_neg_integer()
line_or_bytes: :line | pos_integer()
}
@doc false
+12 -11
View File
@@ -24,7 +24,7 @@ defprotocol JSON.Encoder do
> #### 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.
> new fields are added. Other approaches should be used with caution.
You can also use `Protocol.derive/3` if you don't own the struct that you want
to encode to JSON:
@@ -67,7 +67,7 @@ defprotocol JSON.Encoder do
{io, _prefix} =
Enum.flat_map_reduce(kv, ?{, fn {field, value}, prefix ->
key = IO.iodata_to_binary([prefix, :elixir_json.encode_binary(Atom.to_string(field)), ?:])
key = IO.iodata_to_binary([prefix, :json.encode_binary(Atom.to_string(field)), ?:])
{[key, quote(do: encoder.(unquote(value), encoder))], ?,}
end)
@@ -130,25 +130,25 @@ end
defimpl JSON.Encoder, for: BitString do
def encode(value, _encoder) do
:elixir_json.encode_binary(value)
:json.encode_binary(value)
end
end
defimpl JSON.Encoder, for: List do
def encode(value, encoder) do
:elixir_json.encode_list(value, encoder)
:json.encode_list(value, encoder)
end
end
defimpl JSON.Encoder, for: Integer do
def encode(value, _encoder) do
:elixir_json.encode_integer(value)
:json.encode_integer(value)
end
end
defimpl JSON.Encoder, for: Float do
def encode(value, _encoder) do
:elixir_json.encode_float(value)
:json.encode_float(value)
end
end
@@ -175,6 +175,7 @@ defimpl JSON.Encoder, for: Map do
# Erlang supports only numbers, binaries, and atoms as keys,
# we support anything that implements the String.Chars protocol.
@compile inline: [key: 2]
defp key(key, encoder) when is_atom(key), do: encoder.(Atom.to_string(key), encoder)
defp key(key, encoder) when is_binary(key), do: encoder.(key, encoder)
defp key(key, encoder), do: encoder.(String.Chars.to_string(key), encoder)
@@ -408,7 +409,7 @@ defmodule JSON do
decoders = Keyword.put_new(decoders, :null, nil)
try do
:elixir_json.decode(binary, acc, Map.new(decoders))
:json.decode(binary, acc, Map.new(decoders))
catch
:error, :unexpected_end ->
{:error, {:unexpected_end, byte_size(binary)}}
@@ -528,16 +529,16 @@ defmodule JSON do
end
def protocol_encode(value, _encoder) when is_binary(value),
do: :elixir_json.encode_binary(value)
do: :json.encode_binary(value)
def protocol_encode(value, _encoder) when is_integer(value),
do: :elixir_json.encode_integer(value)
do: :json.encode_integer(value)
def protocol_encode(value, _encoder) when is_float(value),
do: :elixir_json.encode_float(value)
do: :json.encode_float(value)
def protocol_encode(value, encoder) when is_list(value),
do: :elixir_json.encode_list(value, encoder)
do: :json.encode_list(value, encoder)
def protocol_encode(%{} = value, encoder) when not is_map_key(value, :__struct__),
do: JSON.Encoder.Map.encode(value, encoder)
+109 -89
View File
@@ -142,7 +142,7 @@ defmodule Kernel do
* [Patterns and guards](patterns-and-guards.md) - an introduction to patterns,
guards, and extensions
* [Syntax reference](syntax-reference.md) - the language syntax reference
* [Typespecs reference](typespecs.md)- types and function specifications, including list of types
* [Typespecs reference](typespecs.md) - types and function specifications, including list of types
* [Unicode syntax](unicode-syntax.md) - outlines Elixir support for Unicode
## Guards
@@ -2069,15 +2069,20 @@ defmodule Kernel do
end
defp build_boolean_check(operator, check, true_clause, false_clause) do
annotate_case(
[optimize_boolean: true, type_check: :expr],
bools =
quote do
case unquote(check) do
false -> unquote(false_clause)
true -> unquote(true_clause)
other -> :erlang.error({:badbool, unquote(operator), other})
end
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]]}
)
end
@@ -2104,7 +2109,7 @@ defmodule Kernel do
assert_no_match_or_guard_scope(__CALLER__.context, "!")
annotate_case(
[optimize_boolean: true, type_check: :expr],
[optimize_boolean: true, type_check: {:case, :"!!"}],
quote do
case unquote(value) do
x when unquote(x_is_false_or_nil()) -> false
@@ -2118,7 +2123,7 @@ defmodule Kernel do
assert_no_match_or_guard_scope(__CALLER__.context, "!")
annotate_case(
[optimize_boolean: true, type_check: :expr],
[optimize_boolean: true, type_check: {:case, :!}],
quote do
case unquote(value) do
x when unquote(x_is_false_or_nil()) -> true
@@ -2747,7 +2752,7 @@ defmodule Kernel do
nil ->
quote do
case unquote(term) do
%_{__exception__: true} -> true
%_{__exception__: _} -> true
_ -> false
end
end
@@ -2759,8 +2764,7 @@ 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)) and
:erlang.map_get(:__exception__, unquote(term)) == true
:erlang.is_map_key(:__exception__, unquote(term))
end
end
end
@@ -2787,7 +2791,7 @@ defmodule Kernel do
case unquote(name) do
name when is_atom(name) ->
case unquote(term) do
%{__struct__: ^name, __exception__: true} -> true
%{__struct__: ^name, __exception__: _} -> true
_ -> false
end
@@ -2805,8 +2809,7 @@ 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)) and
:erlang.map_get(:__exception__, unquote(term)) == true
:erlang.is_map_key(:__exception__, unquote(term))
end
end
end
@@ -3085,7 +3088,7 @@ defmodule Kernel do
@doc """
Pops a key from the given nested structure.
Uses the `Access` protocol to traverse the structures
Uses the `Access` behaviour to traverse the structures
according to the given `keys`, unless the `key` is a
function. If the key is a function, it will be invoked
as specified in `get_and_update_in/3`.
@@ -4049,7 +4052,7 @@ defmodule Kernel do
defp build_if(condition, do: do_clause, else: else_clause) do
annotate_case(
[optimize_boolean: true, type_check: :expr],
[optimize_boolean: true, type_check: {:case, :if}],
quote do
case unquote(condition) do
x when unquote(x_is_false_or_nil()) -> unquote(else_clause)
@@ -4100,9 +4103,15 @@ defmodule Kernel do
end
defp build_unless(condition, do: do_clause, else: else_clause) do
quote do
if(unquote(condition), do: unquote(else_clause), else: unquote(do_clause))
end
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
)
end
defp build_unless(_condition, _arguments) do
@@ -4370,7 +4379,7 @@ defmodule Kernel do
assert_no_match_or_guard_scope(__CALLER__.context, "&&")
annotate_case(
[type_check: :expr],
[type_check: {:case, :&&}],
quote do
case unquote(left) do
x when unquote(x_is_false_or_nil()) ->
@@ -4413,7 +4422,7 @@ defmodule Kernel do
assert_no_match_or_guard_scope(__CALLER__.context, "||")
annotate_case(
[type_check: :expr],
[type_check: {:case, :||}],
quote do
case unquote(left) do
x when unquote(x_is_false_or_nil()) ->
@@ -4694,17 +4703,13 @@ defmodule Kernel do
false
[] ->
quote do
_ = unquote(left)
false
end
# inlined as false in erlang pass
quote(do: :lists.member(unquote(left), []))
[head | tail] = list ->
# 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)))
case in_body? do
false -> in_list(left, head, tail, expand, list)
true -> quote(do: :lists.member(unquote(left), unquote(right)))
end
%{} = right ->
@@ -4716,7 +4721,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.member?(unquote(right), unquote(left)))
quote(do: Elixir.Enum.__in__(unquote(left), unquote(right)))
_ ->
raise_on_invalid_args_in_2(right)
@@ -4751,12 +4756,6 @@ 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
@@ -4816,12 +4815,12 @@ defmodule Kernel do
end
end
defp in_list(left, head, tail, expand, right, in_body?) do
[head | tail] = :lists.map(&comp(left, &1, expand, right, in_body?), [head | tail])
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)
end
defp comp(left, {:|, _, [head, tail]}, expand, right, in_body?) do
defp comp(left, {:|, _, [head, tail]}, expand, right) do
case expand.(tail) do
[] ->
quote(do: :erlang."=:="(unquote(left), unquote(head)))
@@ -4830,15 +4829,7 @@ defmodule Kernel do
quote do
Kernel.or(
:erlang."=:="(unquote(left), unquote(head)),
unquote(in_list(left, tail_head, tail, expand, right, in_body?))
)
end
tail when in_body? ->
quote do
Kernel.or(
:erlang."=:="(unquote(left), unquote(head)),
:lists.member(unquote(left), unquote(tail))
unquote(in_list(left, tail_head, tail, expand, right))
)
end
@@ -4847,7 +4838,7 @@ defmodule Kernel do
end
end
defp comp(left, right, _expand, _right, _in_body?) do
defp comp(left, right, _expand, _right) do
quote(do: :erlang."=:="(unquote(left), unquote(right)))
end
@@ -4969,6 +4960,7 @@ defmodule Kernel do
"""
@doc since: "1.14.0"
@spec binary_slice(binary, integer, non_neg_integer) :: binary
def binary_slice(binary, start, size)
when is_binary(binary) and is_integer(start) and is_integer(size) and size >= 0 do
total = byte_size(binary)
@@ -5042,6 +5034,7 @@ defmodule Kernel do
"""
@doc since: "1.14.0"
@spec binary_slice(binary, Range.t()) :: binary
def binary_slice(binary, first..last//step)
when is_binary(binary) and step > 0 do
total = byte_size(binary)
@@ -5156,7 +5149,7 @@ defmodule Kernel do
warning saying that a module has been redefined.
There are some modules that Elixir does not currently implement but it
may be implement in the future. Those modules are reserved and defining
may implement in the future. Those modules are reserved and defining
them will result in a compilation error:
defmodule Any do
@@ -5223,6 +5216,11 @@ 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: []
@@ -5252,12 +5250,12 @@ defmodule Kernel do
# defmodule Alias nested
defp alias_defmodule({:__aliases__, _, [h | t]}, _module, env) when is_atom(h) do
module = :elixir_aliases.concat([env.module, h])
alias = String.to_atom("Elixir." <> Atom.to_string(h))
alias = String.to_unsafe_atom("Elixir." <> Atom.to_string(h))
opts = [as: alias, warn: false]
case t do
[] -> {module, module, opts}
_ -> {String.to_atom(Enum.join([module | t], ".")), module, opts}
_ -> {String.to_unsafe_atom(Enum.join([module | t], ".")), module, opts}
end
end
@@ -5564,7 +5562,7 @@ defmodule Kernel do
when the struct is printed:
defmodule User do
@derive {Inspect, only: :name}
@derive {Inspect, only: [:name]}
defstruct name: nil, age: nil
end
@@ -5591,6 +5589,9 @@ defmodule Kernel do
defstruct name: nil, age: 10 + 11
end
`@enforce_keys` must be set to an atom or a list of unique atoms,
all of which must name fields defined by `defstruct/1`
Now trying to build a struct without the name key will fail:
%User{age: 21}
@@ -5875,11 +5876,15 @@ defmodule Kernel do
end
@doc """
Defines a macro suitable for use in guard expressions.
Defines a custom guard with the given name.
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.
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).
When defining your own guards, consider the
[naming conventions](naming-conventions.html#is_-prefix-is_foo)
@@ -5887,31 +5892,30 @@ 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
defmodule Collatz do
@moduledoc "Tools for working with the Collatz sequence."
import Integer.Guards
which can then be used as:
@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)
require Integer.Guards
Integer.Guards.is_even(3)
#=> false
defp step(1, step_count) do
step_count
end
## Implementation details
defp step(n, step_count) when is_even(n) do
step(div(n, 2), step_count + 1)
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) do
step(3 * n + 1, step_count + 1)
end
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.
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()
@@ -6279,7 +6283,7 @@ defmodule Kernel do
step through the code it sees). For general stepping, you can set breakpoints
using `IEx.break!/4`.
For more information, [see IEx documentation](https://hexdocs.pm/iex/IEx.html#module-dbg-and-breakpoints).
For more information, [see IEx documentation](https://iex.hexdocs.pm/IEx.html#module-dbg-and-breakpoints).
## Configuring the debug function
@@ -6326,7 +6330,15 @@ defmodule Kernel do
"""
@doc since: "1.14.0"
defmacro dbg(code \\ quote(do: binding()), options \\ []) do
{mod, fun, args} = Application.compile_env!(__CALLER__, :elixir, :dbg_callback)
# 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
Macro.compile_apply(mod, fun, [code, options, __CALLER__ | args], __CALLER__)
end
@@ -6370,7 +6382,7 @@ defmodule Kernel do
### Passing timeouts
You can also pass timeouts directly to this functions, that is, milliseconds or
You can also pass timeouts directly to this function, that is, milliseconds or
the atom `:infinity`. In this case, this function just returns the given argument.
## Examples
@@ -6415,12 +6427,20 @@ defmodule Kernel do
{microsecond, _precision} = duration.microsecond
millisecond = :erlang.convert_time_unit(microsecond, :microsecond, :millisecond)
duration.week * unquote(week_in_ms) +
duration.day * unquote(day_in_ms) +
duration.hour * unquote(hour_in_ms) +
duration.minute * 60_000 +
duration.second * 1000 +
millisecond
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
end
end
@@ -6882,7 +6902,7 @@ defmodule Kernel do
defp maybe_atomize_calendar(<<alias, _::binary>> = last_part, string)
when alias >= ?A and alias <= ?Z do
string = binary_part(string, 0, byte_size(string) - byte_size(last_part) - 1)
{String.to_atom("Elixir." <> last_part), string}
{String.to_unsafe_atom("Elixir." <> last_part), string}
end
defp maybe_atomize_calendar(_last_part, string) do
@@ -6993,7 +7013,7 @@ defmodule Kernel do
case mod do
?s -> parts
?a -> :lists.map(&String.to_atom/1, parts)
?a -> :lists.map(&String.to_unsafe_atom/1, parts)
?c -> :lists.map(&String.to_charlist/1, parts)
end
@@ -7002,7 +7022,7 @@ defmodule Kernel do
case mod do
?s -> parts
?a -> quote(do: :lists.map(&String.to_atom/1, unquote(parts)))
?a -> quote(do: :lists.map(&String.to_unsafe_atom/1, unquote(parts)))
?c -> quote(do: :lists.map(&String.to_charlist/1, unquote(parts)))
end
end
@@ -7036,7 +7056,7 @@ defmodule Kernel do
:guard ->
raise ArgumentError,
"invalid expression in guard, #{exp} is not allowed in guards. " <>
"To learn more about guards, visit: https://hexdocs.pm/elixir/patterns-and-guards.html"
"To learn more about guards, visit: https://elixir.hexdocs.pm/patterns-and-guards.html"
_ ->
:ok
+13 -7
View File
@@ -49,7 +49,7 @@ defmodule Kernel.CLI do
@doc """
Runs the given function by catching any failure
and printing them to stdout. `at_exit` hooks are
and printing them to stderr. `at_exit` hooks are
also invoked before exiting.
This function is used by Elixir's CLI and also
@@ -99,9 +99,14 @@ defmodule Kernel.CLI do
Shared helper for error formatting on CLI tools.
"""
def format_error(kind, reason, stacktrace) do
{banner, rest} = format_error_parts(kind, reason, stacktrace)
[banner, rest]
end
defp format_error_parts(kind, reason, stacktrace) do
{blamed, stacktrace} = Exception.blame(kind, reason, stacktrace)
iodata =
banner =
case blamed do
%FunctionClauseError{} ->
formatted = Exception.format_banner(kind, reason, stacktrace)
@@ -112,7 +117,7 @@ defmodule Kernel.CLI do
Exception.format_banner(kind, blamed, stacktrace)
end
[iodata, ?\n, Exception.format_stacktrace(prune_stacktrace(stacktrace))]
{banner, [?\n, Exception.format_stacktrace(prune_stacktrace(stacktrace))]}
end
@doc """
@@ -179,7 +184,8 @@ defmodule Kernel.CLI do
## Error handling
defp print_error(kind, reason, stacktrace) do
IO.write(:stderr, format_error(kind, reason, stacktrace))
{banner, rest} = format_error_parts(kind, reason, stacktrace)
IO.write(:stderr, [IO.ANSI.format([:red, banner]), rest])
end
defp blame_match(%{match?: true, node: node}), do: blame_ansi(:normal, "+", node)
@@ -200,7 +206,7 @@ defmodule Kernel.CLI do
end
@elixir_internals [:elixir, :elixir_aliases, :elixir_clauses, :elixir_compiler, :elixir_def] ++
[:elixir_def, :elixir_dispatch, :elixir_expand, :elixir_lexical] ++
[:elixir_dispatch, :elixir_expand, :elixir_lexical] ++
[:elixir_map, :elixir_module] ++
[:elixir_erl, :elixir_erl_clauses, :elixir_erl_compiler, :elixir_erl_pass] ++
[Kernel.ErrorHandler, Module.ParallelChecker]
@@ -338,7 +344,7 @@ defmodule Kernel.CLI do
parse_argv(t, %{config | verbose_compile: true})
end
defp parse_argv([~c"--profile", "time" | t], %{mode: :elixirc} = config) do
defp parse_argv([~c"--profile", ~c"time" | t], %{mode: :elixirc} = config) do
parse_argv(t, %{config | profile: :time})
end
@@ -427,7 +433,7 @@ defmodule Kernel.CLI do
end
defp process_command({:rpc_eval, node, expr}, _config) when is_list(expr) do
node = List.to_atom(node)
node = List.to_unsafe_atom(node)
# Explicitly connect the node in case the rpc node was started with --sname/--name undefined.
_ = :net_kernel.connect_node(node)
+3 -3
View File
@@ -23,7 +23,7 @@ defmodule Kernel.LexicalTracker do
@doc """
Invoked during module expansion to annotate a require
must be warned if unused.
that must be warned if unused.
"""
def warn_require(pid, meta, module, alias) do
:gen_server.cast(pid, {:warn_require, module, meta, alias})
@@ -32,7 +32,7 @@ defmodule Kernel.LexicalTracker do
@doc """
Invoked during module expansion to annotate an alias
must be warned if unused.
that must be warned if unused.
"""
def warn_alias(pid, meta, alias, module) do
:gen_server.cast(pid, {:warn_alias, alias, meta})
@@ -41,7 +41,7 @@ defmodule Kernel.LexicalTracker do
@doc """
Invoked during module expansion to annotate an import
must be warned if unused.
that must be warned if unused.
"""
def warn_import(pid, module) do
:gen_server.cast(pid, {:warn_import, module})
+44 -24
View File
@@ -25,9 +25,8 @@ 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: ([module()], [Code.diagnostic(:warning)] ->
{:compile, [module()], [Code.diagnostic(:warning)]}
| {:runtime, [module()], [Code.diagnostic(:warning)]}),
each_cycle: (-> {:compile, [Path.t()], [Code.diagnostic(:warning)]}
| {:runtime, [{module(), Path.t()}], [Code.diagnostic(:warning)]}),
long_compilation_threshold: pos_integer(),
long_verification_threshold: pos_integer(),
verification: boolean(),
@@ -35,7 +34,8 @@ defmodule Kernel.ParallelCompiler do
dest: Path.t(),
beam_timestamp: term(),
return_diagnostics: boolean(),
max_concurrency: pos_integer()
max_concurrency: pos_integer(),
purge_compiler_modules: boolean()
]
@typedoc """
@@ -100,6 +100,8 @@ defmodule Kernel.ParallelCompiler do
This function allows a developer to perform such tasks.
"""
@doc since: "1.16.0"
@spec pmap(Enumerable.t(input), (input -> output)) :: [output]
when input: term, output: term
def pmap(collection, fun) when is_function(fun, 1) do
ref = make_ref()
@@ -171,25 +173,25 @@ defmodule Kernel.ParallelCompiler do
* `:each_long_verification` (since v1.19.0) - for each file that takes more
than a given timeout (see the `:long_verification_threshold` option) to
compile, invoke this callback passing the module as its argument (and
verify, invoke this callback passing the module as its argument (and
optionally the PID of the process verifying the module)
* `:each_module` - for each module compiled, invokes the callback passing
the file, module and the module bytecode
* `:each_cycle` - after the given files are compiled, invokes this function
that should return the following values:
* `:each_cycle` - invoked after each compilation cycle and should return one
of the following values:
* `{:compile, modules, warnings}` - to continue compilation with a list of
further modules to compile
further module files to compile
* `{:runtime, modules, warnings}` - to stop compilation and verify the list
of modules because dependent modules have changed
of `{module, path}` pairs because dependent modules have changed
* `:long_compilation_threshold` - the timeout (in seconds) to check for files
taking too long to compile. For each file that exceeds the threshold, the
`:each_long_compilation` callback is invoked. Defaults to `10` seconds.
* `:long_verification_threshold` (since v1.19.0) - the timeout (in seconds) to
check for modules taking too long to compile. For each module that exceeds the
check for modules taking too long to verify. For each module that exceeds the
threshold, the `:each_long_verification` callback is invoked. Defaults to
`10` seconds.
@@ -197,8 +199,11 @@ 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
and group pass checker
* `: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`)
* `:dest` - the destination directory for the BEAM files. When using `compile/2`,
this information is only used to properly annotate the BEAM files before
@@ -209,7 +214,7 @@ defmodule Kernel.ParallelCompiler do
* `:return_diagnostics` (since v1.15.0) - returns maps with information instead of
a list of warnings and returns diagnostics as maps instead of tuples.
This option must be set to true, except for backwards compatibibility reasons.
This option must be set to true, except for backward compatibility reasons.
* `:max_concurrency` - the maximum number of files to compile in parallel.
Setting this option to 1 will compile files sequentially.
@@ -266,7 +271,7 @@ defmodule Kernel.ParallelCompiler do
* `:return_diagnostics` (since v1.15.0) - returns maps with information instead of
a list of warnings and returns diagnostics as maps instead of tuples.
This option must be set to true, except for backwards compatibibility reasons.
This option must be set to true, except for backward compatibility reasons.
"""
@doc since: "1.6.0"
@@ -336,7 +341,14 @@ defmodule Kernel.ParallelCompiler do
defp spawn_workers(schedulers, checker, files, output, options) do
threshold = Keyword.get(options, :long_compilation_threshold, 10) * 1000
timer_ref = Process.send_after(self(), :threshold_check, threshold)
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
{outcome, state} =
spawn_workers(files, %{}, %{}, [], %{}, [], [], %{
@@ -353,7 +365,8 @@ defmodule Kernel.ParallelCompiler do
long_compilation_threshold: threshold,
schedulers: schedulers,
checker: checker,
verification?: Keyword.get(options, :verification, true)
verification?: Keyword.get(options, :verification, true),
purge_compiler_modules: purge_compiler_modules
})
Process.cancel_timer(state.timer_ref)
@@ -448,7 +461,7 @@ defmodule Kernel.ParallelCompiler do
modules = write_module_binaries(result, state.output, state)
profile(state, "after compile callback", state.after_compile)
runtime_warnings =
{runtime_warnings, errors} =
if state.verification? do
profile(
state,
@@ -459,11 +472,19 @@ 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}
{{:ok, modules, info}, state}
case errors do
[] ->
{{:ok, modules, info}, state}
_ ->
IO.puts(:stderr, "== Type checking failed with errors ==")
{{:error, errors, info}, state}
end
end
defp profile_init(:time), do: {:time, System.monotonic_time(), 0}
@@ -570,11 +591,11 @@ defmodule Kernel.ParallelCompiler do
case cycle_return do
{:runtime, dependent_modules, extra_warnings} ->
:elixir_code_server.cast(:purge_compiler_modules)
state.purge_compiler_modules.()
verify_modules(result, extra_warnings ++ warnings, dependent_modules, state)
{:compile, [], extra_warnings} ->
:elixir_code_server.cast(:purge_compiler_modules)
state.purge_compiler_modules.()
verify_modules(result, extra_warnings ++ warnings, [], state)
{:compile, more, extra_warnings} ->
@@ -821,7 +842,7 @@ defmodule Kernel.ParallelCompiler do
end
end
timer_ref = Process.send_after(self(), :threshold_check, state.long_compilation_threshold)
timer_ref = :erlang.send_after(state.long_compilation_threshold, self(), :threshold_check)
state = %{state | timer_ref: timer_ref}
spawn_workers(queue, spawned, waiting, files, result, warnings, errors, state)
@@ -879,8 +900,7 @@ defmodule Kernel.ParallelCompiler do
end
defp return_error(warnings, errors, state, fun) do
# Also prune compiler modules in case of errors
:elixir_code_server.cast(:purge_compiler_modules)
state.purge_compiler_modules.()
errors =
Enum.map(errors, fn {%{file: file} = diagnostic, read_snippet} ->
+7 -7
View File
@@ -768,8 +768,8 @@ defmodule Kernel.SpecialForms do
It is used in typespecs to specify the type of a variable,
function or of a type itself:
@type number :: integer | float
@spec add(number, number) :: number
@type num :: integer | float
@spec add(num, num) :: num
It may also be used in bit strings to specify the type
of a given bit segment:
@@ -1333,9 +1333,9 @@ defmodule Kernel.SpecialForms do
sum(1, value, 3)
end
Which the argument for the `:sum` function call is not the
expected result:
However, the code above does not work as expected, because this
injects the representation of the `value` variable,
not its contents:
{:sum, [], [1, {:value, [], Elixir}, 3]}
@@ -2077,9 +2077,9 @@ defmodule Kernel.SpecialForms do
iex> try do
...> 1 / 0
...> rescue
...> x in [ArithmeticError] -> [:rescued, is_exception(x)]
...> x in [ArithmeticError] -> {:rescued, Exception.message(x)}
...> end
[:rescued, true]
{:rescued, "bad argument in arithmetic expression"}
Rescue different errors with separate clauses:
+36 -15
View File
@@ -255,9 +255,20 @@ defmodule Kernel.Typespec do
case type_to_signature(expr) do
{name, arity} = type_pair ->
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)
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
end
if Map.has_key?(type_pairs, type_pair) do
@@ -599,8 +610,7 @@ defmodule Kernel.Typespec do
types =
:lists.map(
fn %{field: field} ->
default_type = if field == :__exception__, do: true, else: quote(do: term())
{field, Keyword.get(fields, field, default_type)}
{field, Keyword.get(fields, field, quote(do: term()))}
end,
struct_info
)
@@ -676,6 +686,13 @@ defmodule Kernel.Typespec do
{{:type, location(meta), :range, [left, right]}, state}
end
defp typespec({:..//, _meta, [_first, _last, _step]} = range, _vars, caller, _state) do
compile_error(
caller,
"ranges with steps are not supported in typespecs, got: #{Macro.to_string(range)}"
)
end
# Handle special forms
defp typespec({:__MODULE__, _, atom}, vars, caller, state) when is_atom(atom) do
typespec(caller.module, vars, caller, state)
@@ -850,29 +867,29 @@ defmodule Kernel.Typespec do
{{:type, location(meta), :nonempty_string, args}, state}
end
defp typespec({type, _meta, []}, vars, caller, state) when type in [:charlist, :char_list] do
defp typespec({type, meta, []}, vars, caller, state) when type in [:charlist, :char_list] do
if type == :char_list do
warning = "the char_list() type is deprecated, use charlist()"
IO.warn(warning, caller)
end
typespec(quote(do: :elixir.charlist()), vars, caller, state)
remote_typespec(:charlist, meta, [], vars, caller, state)
end
defp typespec({:nonempty_charlist, _meta, []}, vars, caller, state) do
typespec(quote(do: :elixir.nonempty_charlist()), vars, caller, state)
defp typespec({:nonempty_charlist, meta, []}, vars, caller, state) do
remote_typespec(:nonempty_charlist, meta, [], vars, caller, state)
end
defp typespec({:struct, _meta, []}, vars, caller, state) do
typespec(quote(do: :elixir.struct()), vars, caller, state)
defp typespec({:struct, meta, []}, vars, caller, state) do
remote_typespec(:struct, meta, [], vars, caller, state)
end
defp typespec({:as_boolean, _meta, [arg]}, vars, caller, state) do
typespec(quote(do: :elixir.as_boolean(unquote(arg))), vars, caller, state)
defp typespec({:as_boolean, meta, [arg]}, vars, caller, state) do
remote_typespec(:as_boolean, meta, [arg], vars, caller, state)
end
defp typespec({:keyword, _meta, args}, vars, caller, state) when length(args) <= 1 do
typespec(quote(do: :elixir.keyword(unquote_splicing(args))), vars, caller, state)
defp typespec({:keyword, meta, args}, vars, caller, state) when length(args) <= 1 do
remote_typespec(:keyword, meta, args, vars, caller, state)
end
defp typespec({:fun, meta, args}, vars, caller, state) do
@@ -997,6 +1014,10 @@ defmodule Kernel.Typespec do
{{:remote_type, location(meta), [remote, name, args]}, state}
end
defp remote_typespec(name, meta, args, vars, caller, state) do
typespec({{:., meta, [:elixir, name]}, meta, args}, vars, caller, state)
end
defp collect_union({:|, _, [a, b]}), do: [a | collect_union(b)]
defp collect_union(v), do: [v]
+19 -22
View File
@@ -162,16 +162,8 @@ defmodule Kernel.Utils do
# TODO: Make it raise on v2.0
warn_on_duplicate_struct_key(:lists.keysort(1, fields), env)
foreach = fn
key when is_atom(key) ->
:ok
key ->
raise ArgumentError, "keys given to @enforce_keys must be atoms, got: #{inspect(key)}"
end
:lists.foreach(foreach, enforce_keys)
struct = :maps.from_list([__struct__: module] ++ fields)
field_map = :maps.from_list(fields)
struct = :maps.put(:__struct__, module, field_map)
escaped_struct = :elixir_quote.escape(struct, {:struct, module}, false)
body =
@@ -215,7 +207,7 @@ defmodule Kernel.Utils do
end
end
case enforce_keys -- :maps.keys(struct) do
case enforce_keys -- :maps.keys(field_map) do
[] ->
mapper = fn {key, val} ->
%{field: key, default: val, required: :lists.member(key, enforce_keys)}
@@ -225,10 +217,9 @@ defmodule Kernel.Utils do
derive = :lists.map(fn {_, value} -> value end, :ets.take(bag, {:accumulate, :derive}))
{struct, :lists.reverse(derive), escaped_struct, quote(do: kv), body}
error_keys ->
invalid_keys ->
raise ArgumentError,
"@enforce_keys required keys (#{inspect(error_keys)}) that are not defined in defstruct: " <>
"#{inspect(fields)}"
"unknown or duplicate keys given to @enforce_keys, got: #{inspect(invalid_keys)}"
end
end
@@ -266,7 +257,7 @@ defmodule Kernel.Utils do
module.exception([])
end
def raise(%_{__exception__: true} = exception) do
def raise(%_{__exception__: _} = exception) do
exception
end
@@ -329,21 +320,27 @@ defmodule Kernel.Utils do
@spec defguard([Macro.t()], Macro.t(), Macro.Env.t()) :: Macro.t()
def defguard(args, expr, env) do
{^args, vars} = extract_refs_from_args(args)
env = :elixir_env.with_vars(%{env | context: :guard}, vars)
{expr, _, _} = :elixir_expand.expand(expr, :elixir_env.env_to_ex(env), env)
{_, vars} = extract_refs_from_args(args)
guard_expr = expand_defguard(expr, %{env | context: :guard}, vars)
body_expr = expand_defguard(expr, %{env | context: nil}, vars)
quote do
case Macro.Env.in_guard?(__CALLER__) do
true ->
unquote(literal_quote(unquote_every_ref(expr, vars), []))
unquote(literal_quote(unquote_every_ref(guard_expr, vars), []))
false ->
unquote(literal_quote(unquote_refs_once(expr, vars, env.module), generated: true))
unquote(literal_quote(unquote_refs_once(body_expr, vars, env), generated: true))
end
end
end
defp expand_defguard(expr, env, vars) do
env = :elixir_env.with_vars(env, vars)
{expr, _, _} = :elixir_expand.expand(expr, :elixir_env.env_to_ex(env), env)
expr
end
defp extract_refs_from_args(args) do
Macro.postwalk(args, [], fn
{ref, meta, context} = var, acc when is_atom(ref) and is_atom(context) ->
@@ -369,7 +366,7 @@ defmodule Kernel.Utils do
end
# Prefaces `guard` with unquoted versions of `refs`.
defp unquote_refs_once(guard, refs, module) do
defp unquote_refs_once(guard, refs, %{module: module}) do
{guard, used_refs} =
Macro.postwalk(guard, %{}, fn
{ref, meta, context} = var, acc when is_atom(ref) and is_atom(context) ->
@@ -382,7 +379,7 @@ defmodule Kernel.Utils do
{new_var, acc}
%{} ->
generated = String.to_atom("arg" <> Integer.to_string(map_size(acc) + 1))
generated = String.to_unsafe_atom("arg" <> Integer.to_string(map_size(acc) + 1))
new_var = Macro.unique_var(generated, module)
{new_var, Map.put(acc, pair, {new_var, var})}
end
+69 -37
View File
@@ -38,7 +38,7 @@ defmodule Keyword do
## Duplicate keys and ordering
A keyword may have duplicate keys so it is not strictly a key-value
A keyword list may have duplicate keys so it is not strictly a key-value
data type. However, most of the functions in this module work on a
key-value structure and behave similar to the functions you would
find in the `Map` module. For example, `Keyword.get/3` will get the first
@@ -119,14 +119,23 @@ 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(&{&1, value}, keys)
: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
)
end
@doc """
@@ -260,38 +269,60 @@ defmodule Keyword do
@doc since: "1.13.0"
@spec validate(keyword(), values :: [atom() | {atom(), term()}]) ::
{:ok, keyword()} | {:error, [atom]}
def validate([], values) when is_list(values), do: {:ok, move_pairs!(values, [])}
def validate(keyword, values) when is_list(keyword) and is_list(values) do
validate(keyword, values, [], [], [])
validate_merge(keyword, values, [], keyword)
end
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, [pair | acc], bad_keys)
defp validate_merge([], values, values_pre, original),
do: {:ok, move_pairs!(values, move_pairs!(values_pre, original))}
:error ->
case find_key!(key, values2, values1) do
{values1, values2} ->
validate(keyword, values1, values2, [pair | acc], bad_keys)
:error ->
validate(keyword, values1, values2, acc, [key | bad_keys])
end
defp validate_merge([{key, _} = pair | keyword], [head | tail], values_pre, original)
when is_atom(key) do
case head do
^key -> validate_merge(keyword, tail, values_pre, original)
{^key, _} -> validate_merge(keyword, tail, values_pre, original)
_ -> validate_merge([pair | keyword], tail, [head | values_pre], original)
end
end
defp validate([], values1, values2, acc, []) do
{:ok, move_pairs!(values1, move_pairs!(values2, acc))}
defp validate_merge([{key, _} | keyword], [], values_pre, original) when is_atom(key) do
case find_key!(key, values_pre, []) do
{new_values, new_values_pre} ->
validate_merge(keyword, new_values, new_values_pre, original)
:error ->
validate_fallback(keyword, values_pre, [key])
end
end
defp validate([], _values1, _values2, _acc, bad_keys) do
{:error, bad_keys}
defp validate_merge([pair | _], _, _, _),
do:
raise(
ArgumentError,
"expected a keyword list as first argument, got invalid entry: #{inspect(pair)}"
)
defp validate_fallback([{key, _} | keyword], values, bad_keys)
when is_atom(key) do
case find_key!(key, values, []) do
{rest, acc} ->
validate_fallback(keyword, rest ++ acc, bad_keys)
:error ->
validate_fallback(keyword, values, [key | bad_keys])
end
end
defp validate([pair | _], _values1, _values2, _acc, []) do
raise ArgumentError,
"expected a keyword list as first argument, got invalid entry: #{inspect(pair)}"
end
defp validate_fallback([], _, bad), do: {:error, bad}
defp validate_fallback([p | _], _, _),
do:
raise(
ArgumentError,
"expected a keyword list as first argument, got invalid entry: #{inspect(p)}"
)
defp find_key!(key, [key | rest], acc), do: {rest, acc}
defp find_key!(key, [{key, _} | rest], acc), do: {rest, acc}
@@ -486,7 +517,7 @@ defmodule Keyword do
{get, :lists.reverse(acc, [{key, value} | delete(t, key)])}
:pop ->
{current, :lists.reverse(acc, t)}
{current, :lists.reverse(acc, delete(t, key))}
other ->
raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}"
@@ -552,7 +583,7 @@ defmodule Keyword do
{get, :lists.reverse(acc, [{key, value} | delete(t, key)])}
:pop ->
{value, :lists.reverse(acc, t)}
{value, :lists.reverse(acc, delete(t, key))}
other ->
raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}"
@@ -956,7 +987,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 `===/3`,
Comparison between values is done with `===/2`,
which means integers are not equivalent to floats:
iex> Keyword.equal?([a: 1.0], [a: 1])
@@ -1292,7 +1323,8 @@ defmodule Keyword do
@doc """
Drops the given `keys` from the keyword list.
Removes duplicate keys from the new keyword list.
If a key occurs multiple times, all of its entries are removed when that key
is included in `keys`.
## Examples
@@ -1333,9 +1365,9 @@ defmodule Keyword do
"""
@spec pop(t, key, default) :: {value | default, t}
def pop(keywords, key, default \\ nil) when is_list(keywords) and is_atom(key) do
case fetch(keywords, key) do
{:ok, value} -> {value, delete(keywords, key)}
:error -> {default, keywords}
case :lists.keyfind(key, 1, keywords) do
{^key, value} -> {value, delete_key(keywords, key)}
false -> {default, keywords}
end
end
@@ -1360,9 +1392,9 @@ defmodule Keyword do
@doc since: "1.10.0"
@spec pop!(t, key) :: {value, t}
def pop!(keywords, key) when is_list(keywords) and is_atom(key) do
case fetch(keywords, key) do
{:ok, value} -> {value, delete(keywords, key)}
:error -> raise KeyError, key: key, term: keywords
case :lists.keyfind(key, 1, keywords) do
{^key, value} -> {value, delete_key(keywords, key)}
false -> raise KeyError, key: key, term: keywords
end
end
@@ -1402,7 +1434,7 @@ defmodule Keyword do
do: {values, acc}
@doc """
Lazily returns and removes all values associated with `key` in the keyword list.
Lazily returns the first value for `key` and removes all associated entries in the keyword list.
This is useful if the default value is very expensive to calculate or
generally difficult to set up and tear down again.
@@ -1425,9 +1457,9 @@ defmodule Keyword do
@spec pop_lazy(t, key, (-> value)) :: {value, t}
def pop_lazy(keywords, key, fun)
when is_list(keywords) and is_atom(key) and is_function(fun, 0) do
case fetch(keywords, key) do
{:ok, value} -> {value, delete(keywords, key)}
:error -> {fun.(), keywords}
case :lists.keyfind(key, 1, keywords) do
{^key, value} -> {value, delete_key(keywords, key)}
false -> {fun.(), keywords}
end
end
+152 -38
View File
@@ -102,7 +102,7 @@ defmodule List do
Even though the representation changed, the raw data does remain a list of
integers, which can be handled as such:
iex> inspect(~c"abc", charlists: :as_list)
iex> inspect(~c"abc", charlists: :as_lists)
"[97, 98, 99]"
iex> Enum.map(~c"abc", fn num -> 1000 + num end)
[1097, 1098, 1099]
@@ -298,6 +298,30 @@ 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
"""
@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.
@@ -326,6 +350,31 @@ 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
"""
@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
@@ -380,7 +429,7 @@ defmodule List do
"""
@doc since: "1.13.0"
@spec keyfind!([tuple], any, non_neg_integer) :: any
@spec keyfind!([tuple], any, non_neg_integer) :: tuple
def keyfind!(list, key, position) when is_integer(position) do
:lists.keyfind(key, position + 1, list) ||
raise KeyError,
@@ -465,8 +514,8 @@ defmodule List do
As in `Enum.sort/2`, avoid using the default sorting function to sort
structs, as by default it performs structural comparison instead of a
semantic one. In such cases, you shall pass a sorting function as third
element or any module that implements a `compare/2` function. For example,
semantic one. In such cases, you shall pass a sorting function as the third
argument or any module that implements a `compare/2` function. For example,
if you have tuples with user names and their birthday, and you want to
sort on their birthday, in both ascending and descending order, you should
do:
@@ -606,7 +655,7 @@ defmodule List do
end
@doc """
Wraps `term` in a list if this is not list.
Wraps `term` in a list if it is not a list.
If `term` is already a list, it returns the list.
If `term` is `nil`, it returns an empty list.
@@ -905,9 +954,12 @@ defmodule List do
@spec pop_at(list, integer, any) :: {any, list}
def pop_at(list, index, default \\ nil) when is_integer(index) do
if index < 0 do
do_pop_at(list, length(list) + index, default, [])
case length(list) + index do
index when index < 0 -> {default, list}
index -> do_pop_at(list, index, default, [], list)
end
else
do_pop_at(list, index, default, [])
do_pop_at(list, index, default, [], list)
end
end
@@ -969,31 +1021,51 @@ defmodule List do
:lists.suffix(suffix, list)
end
@doc """
Converts a charlist to an atom.
Elixir supports conversions from charlists which contain any Unicode
code point.
Inlined by the compiler.
## Examples
iex> List.to_atom(~c"Elixir")
:Elixir
iex> List.to_atom(~c"🌢 Elixir")
:"🌢 Elixir"
"""
@spec to_atom(charlist) :: atom
@doc deprecated: "Use to_existing_atom/1 or to_unsafe_atom/1 instead"
def to_atom(charlist) do
:erlang.list_to_atom(charlist)
end
@doc """
Converts a charlist to an existing atom or creates a new one.
Elixir supports conversions from charlists which contain any Unicode
code point.
> #### Dynamic Atom Creation {: .warning}
>
> This function creates atoms dynamically and atoms are
> not garbage-collected. Therefore, `charlist` should not be an
> untrusted value, such as input received from a socket or during
> a web request. Consider using `to_existing_atom/1` instead.
By default, the maximum number of atoms is `1_048_576`. This limit
can be raised or lowered using the VM option `+t`.
The maximum atom size is 255 Unicode code points.
Inlined by the compiler.
## Examples
iex> List.to_unsafe_atom(~c"Elixir")
:Elixir
iex> List.to_unsafe_atom(~c"🌢 Elixir")
:"🌢 Elixir"
"""
@doc since: "1.21.0"
@spec to_unsafe_atom(charlist) :: atom
def to_unsafe_atom(charlist) do
:erlang.list_to_atom(charlist)
end
@doc """
Converts a charlist to an existing atom.
If the list of expected atoms is known upfront, prefer `to_existing_atom/2`.
Elixir supports conversions from charlists which contain any Unicode
code point. Raises an `ArgumentError` if the atom does not exist.
@@ -1024,6 +1096,47 @@ defmodule List do
:erlang.list_to_existing_atom(charlist)
end
@doc """
Converts a charlist to one of the `allowed_atoms` or raises.
Raises an `ArgumentError` if the atom either does not exist or is not within
the existing list.
This should be preferred to `to_existing_atom/1` if the list is known upfront,
since there is no risk that the atom has not been loaded.
## Examples
iex> List.to_existing_atom(~c"foo", [:foo, :bar])
:foo
iex> List.to_existing_atom(~c"unknown", [:foo, :bar])
** (ArgumentError) unexpected value: ~c\"unknown\", the allowed atoms are: [:foo, :bar]
"""
@doc since: "1.21.0"
@spec to_existing_atom(charlist, nonempty_list(a)) :: a when a: atom()
def to_existing_atom(charlist, [_ | _] = allowed_atoms) when is_list(charlist) do
atom = :erlang.list_to_existing_atom(charlist)
if atom not in allowed_atoms do
to_existing_atom_unexpected(charlist, allowed_atoms)
end
atom
end
# used just to have a less cryptic stacktrace and consistent error
@doc false
def __to_existing_atom__(charlist, allowed_atoms) do
to_existing_atom_unexpected(charlist, allowed_atoms)
end
defp to_existing_atom_unexpected(charlist, allowed_atoms) do
raise ArgumentError,
"unexpected value: #{inspect(charlist)}, the allowed atoms are: #{inspect(allowed_atoms)}"
end
@doc """
Returns the float whose text representation is `charlist`.
@@ -1035,7 +1148,7 @@ defmodule List do
2.2017764
"""
@spec to_float(charlist) :: float
@spec to_float(nonempty_charlist) :: float
def to_float(charlist) do
:erlang.list_to_float(charlist)
end
@@ -1051,7 +1164,7 @@ defmodule List do
123
"""
@spec to_integer(charlist) :: integer
@spec to_integer(nonempty_charlist) :: integer
def to_integer(charlist) do
:erlang.list_to_integer(charlist)
end
@@ -1069,7 +1182,7 @@ defmodule List do
1023
"""
@spec to_integer(charlist, 2..36) :: integer
@spec to_integer(nonempty_charlist, 2..36) :: integer
def to_integer(charlist, base) do
:erlang.list_to_integer(charlist, base)
end
@@ -1138,7 +1251,7 @@ defmodule List do
Please check the given list or call inspect/1 to get the list representation, got:
#{inspect(list)}
#{inspect(list, charlists: :as_lists)}
"""
else
result when is_binary(result) ->
@@ -1190,7 +1303,7 @@ defmodule List do
Please check the given list or call inspect/1 to get the list representation, got:
#{inspect(list)}
#{inspect(list, charlists: :as_lists)}
"""
else
result when is_list(result) ->
@@ -1255,13 +1368,13 @@ defmodule List do
defp myers_difference_with_diff_script(list1, list2, diff_script) do
path = {0, list1, list2, []}
find_script(0, length(list1) + length(list2), [path], diff_script)
find_script(0, [path], diff_script)
end
defp find_script(envelope, max, paths, diff_script) do
defp find_script(envelope, paths, diff_script) do
case each_diagonal(-envelope, envelope, paths, [], diff_script) do
{:done, edits} -> compact_reverse(edits, [])
{:next, paths} -> find_script(envelope + 1, max, paths, diff_script)
{:next, paths} -> find_script(envelope + 1, paths, diff_script)
end
end
@@ -1406,15 +1519,16 @@ defmodule List do
# pop_at
defp do_pop_at([], _index, default, acc) do
{default, :lists.reverse(acc)}
# The original list is returned when the index is out of bounds
defp do_pop_at([], _index, default, _acc, original) do
{default, original}
end
defp do_pop_at([head | tail], 0, _default, acc) do
defp do_pop_at([head | tail], 0, _default, acc, _original) do
{head, :lists.reverse(acc, tail)}
end
defp do_pop_at([head | tail], index, default, acc) do
do_pop_at(tail, index - 1, default, [head | acc])
defp do_pop_at([head | tail], index, default, acc, original) do
do_pop_at(tail, index - 1, default, [head | acc], original)
end
end
+162 -64
View File
@@ -166,7 +166,7 @@ defmodule Macro do
of a `__block__` or the right side of `->`. The last expression of the
block does not have metadata if it is not followed by an end of line
character (either a newline or `;`). This entry may appear multiple times
in the same metadata if the expression is surround by parens
in the same metadata if the expression is surrounded by parens
* `:format` - set to `:keyword` when an atom is defined as a keyword.
It may also be set to `:atom` to distinguish `nil`, `false`, and `true`
@@ -235,7 +235,7 @@ defmodule Macro do
`div/2` function, so that the AST for that function will become `{:div, [],
[100, 5]}` (`div(100, 5)`).
"""
@spec unpipe(t()) :: [t()]
@spec unpipe(t()) :: [{t(), non_neg_integer}]
def unpipe(expr) do
:lists.reverse(unpipe(expr, []))
end
@@ -518,7 +518,7 @@ defmodule Macro do
@doc since: "1.11.3"
@spec generate_unique_arguments(0, context :: atom) :: []
@spec generate_unique_arguments(pos_integer, context) ::
[{atom, [counter: integer], context}, ...]
[{atom, metadata(), context}, ...]
when context: atom
def generate_unique_arguments(amount, context),
do: generate_arguments(amount, context, &unique_var/2)
@@ -527,7 +527,7 @@ defmodule Macro do
defp generate_arguments(amount, context, fun)
when is_integer(amount) and amount > 0 and is_atom(context) do
for id <- 1..amount, do: fun.(String.to_atom("arg" <> Integer.to_string(id)), context)
for id <- 1..amount, do: fun.(String.to_unsafe_atom("arg" <> Integer.to_string(id)), context)
end
@doc """
@@ -576,7 +576,7 @@ defmodule Macro do
"""
@doc since: "1.11.3"
@spec unique_var(var, context) :: {var, [counter: integer], context}
@spec unique_var(var, context) :: {var, metadata(), context}
when var: atom, context: atom
def unique_var(var, context) when is_atom(var) and is_atom(context) do
{var, [counter: :elixir_module.next_counter(context)], context}
@@ -690,7 +690,39 @@ defmodule Macro do
"""
@spec prewalk(t, (t -> t)) :: t
def prewalk(ast, fun) when is_function(fun, 1) do
elem(prewalk(ast, nil, fn x, nil -> {fun.(x), nil} end), 0)
do_prewalk(fun.(ast), fun)
end
# Mirrors do_traverse/4 with an always-pre fun and no accumulator,
# avoiding the wrapper closures and tuple threading of traverse/4.
# Each clause dispatches on the already-transformed node, so fun's
# rewrites are descended into, exactly as in traverse/4.
defp do_prewalk({form, meta, args}, fun) when is_atom(form) do
{form, meta, do_prewalk_args(args, fun)}
end
defp do_prewalk({form, meta, args}, fun) do
form = do_prewalk(fun.(form), fun)
{form, meta, do_prewalk_args(args, fun)}
end
defp do_prewalk({left, right}, fun) do
left = do_prewalk(fun.(left), fun)
{left, do_prewalk(fun.(right), fun)}
end
defp do_prewalk(list, fun) when is_list(list) do
do_prewalk_args(list, fun)
end
defp do_prewalk(x, _fun) do
x
end
defp do_prewalk_args(args, _fun) when is_atom(args), do: args
defp do_prewalk_args(args, fun) when is_list(args) do
:lists.map(fn x -> do_prewalk(fun.(x), fun) end, args)
end
@doc """
@@ -728,11 +760,41 @@ defmodule Macro do
"""
@spec postwalk(t, (t -> t)) :: t
def postwalk(ast, fun) when is_function(fun, 1) do
elem(postwalk(ast, nil, fn x, nil -> {fun.(x), nil} end), 0)
do_postwalk(ast, fun)
end
# Mirrors do_traverse/4 with an always-post fun and no accumulator,
# avoiding the wrapper closures and tuple threading of traverse/4
defp do_postwalk({form, meta, args}, fun) when is_atom(form) do
fun.({form, meta, do_postwalk_args(args, fun)})
end
defp do_postwalk({form, meta, args}, fun) do
form = do_postwalk(form, fun)
fun.({form, meta, do_postwalk_args(args, fun)})
end
defp do_postwalk({left, right}, fun) do
left = do_postwalk(left, fun)
fun.({left, do_postwalk(right, fun)})
end
defp do_postwalk(list, fun) when is_list(list) do
fun.(do_postwalk_args(list, fun))
end
defp do_postwalk(x, fun) do
fun.(x)
end
defp do_postwalk_args(args, _fun) when is_atom(args), do: args
defp do_postwalk_args(args, fun) when is_list(args) do
:lists.map(fn x -> do_postwalk(x, fun) end, args)
end
@doc """
This functions behaves like `prewalk/3`, but performs a depth-first,
This function behaves like `prewalk/3`, but performs a depth-first,
post-order traversal of quoted expressions using an accumulator.
"""
@spec postwalk(t, any, (t, any -> {t, any})) :: {t, any}
@@ -940,7 +1002,7 @@ defmodule Macro do
This is useful when a struct needs to be expanded at
compilation time and the struct being expanded may or may
not have been compiled (including structs in the defined
not have been compiled (including structs defined
under the module being compiled). For compiled modules,
it will invoke `module.__info__(:struct)`.
@@ -971,9 +1033,15 @@ defmodule Macro do
}
]
def struct_info!(module, env) when is_atom(module) do
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))
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))
end
end
@@ -1028,7 +1096,7 @@ defmodule Macro do
defp find_invalid(other), do: {:error, other}
@doc """
Returns an enumerable that traverses the `ast` in depth-first,
Returns an enumerable that traverses the `ast` in depth-first,
pre-order traversal.
## Examples
@@ -1086,7 +1154,7 @@ defmodule Macro do
end
@doc """
Returns an enumerable that traverses the `ast` in depth-first,
Returns an enumerable that traverses the `ast` in depth-first,
post-order traversal.
## Examples
@@ -1189,14 +1257,15 @@ defmodule Macro do
The mapping function receives an integer representing the code point
of the character it wants to unescape. There are also the special atoms
`:newline`, `:unicode`, and `:hex`, which control newline, unicode,
and escaping respectively.
and escaping respectively, and for which the mapping function must return
a boolean.
Here is the default mapping function implemented by Elixir:
def unescape_map(:newline), do: true
def unescape_map(:unicode), do: true
def unescape_map(:hex), do: true
def unescape_map(?0), do: ?0
def unescape_map(?0), do: 0
def unescape_map(?a), do: ?\a
def unescape_map(?b), do: ?\b
def unescape_map(?d), do: ?\d
@@ -1219,7 +1288,11 @@ defmodule Macro do
Macro.unescape_string("example\\n", &unescape_map(&1))
"""
@spec unescape_string(String.t(), (non_neg_integer -> non_neg_integer | false)) :: String.t()
@spec unescape_string(
String.t(),
(non_neg_integer | :newline | :unicode | :hex ->
non_neg_integer | boolean)
) :: String.t()
def unescape_string(string, map) do
:elixir_interpolation.unescape_string(string, map)
end
@@ -1734,9 +1807,9 @@ defmodule Macro do
defp kw_blocks_to_string(kw, fun) do
Enum.reduce(unquote(kw_keywords), " ", fn x, acc ->
case Keyword.has_key?(kw, x) do
true -> acc <> kw_block_to_string(x, Keyword.get(kw, x), fun)
false -> acc
case Keyword.fetch(kw, x) do
{:ok, value} -> acc <> kw_block_to_string(x, value, fun)
:error -> acc
end
end) <> "end"
end
@@ -1850,6 +1923,7 @@ defmodule Macro do
definition compile-time, and you can use `Macro.expand/2`.
"""
@doc since: "1.16.0"
@spec compile_apply(module(), atom(), [term()], Macro.Env.t()) :: term()
def compile_apply(mod, fun, args, caller) do
:elixir_env.trace({:remote_function, [], mod, fun, length(args)}, %{caller | function: nil})
Kernel.apply(mod, fun, args)
@@ -1974,10 +2048,9 @@ defmodule Macro do
defp do_expand_once({{:., _, [{:__ENV__, _, atom}, field]}, _, []} = original, env)
when is_atom(atom) and is_atom(field) and env.context != :match do
if Map.has_key?(env, field) do
{maybe_escape_map(Map.get(env, field)), true}
else
{original, false}
case Map.fetch(env, field) do
{:ok, value} -> {maybe_escape_map(value), true}
:error -> {original, false}
end
end
@@ -2115,6 +2188,9 @@ defmodule Macro do
def quoted_literal?({:__aliases__, _, args}),
do: quoted_literal?(args)
def quoted_literal?({:__block__, _, [wrapped]}),
do: quoted_literal?(wrapped)
def quoted_literal?({:%, _, [left, right]}),
do: quoted_literal?(left) and quoted_literal?(right)
@@ -2190,7 +2266,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() when acc: term()
@spec expand_literals(t(), acc, (t(), acc -> {t(), acc})) :: {t(), acc} when acc: term()
def expand_literals(ast, acc, fun)
def expand_literals({:__aliases__, meta, args}, acc, fun) do
@@ -2494,7 +2570,7 @@ defmodule Macro do
### As a remote call
Inspect an atom the function name of a remote call.
Inspect an atom as the function name of a remote call.
iex> Macro.inspect_atom(:remote_call, :foo)
"foo"
@@ -2615,55 +2691,77 @@ defmodule Macro do
:unquoted_operator
true ->
charlist = Atom.to_charlist(atom)
if valid_alias?(charlist) do
:alias
else
case :elixir_config.identifier_tokenizer().tokenize(charlist) do
{kind, _acc, [], _, _, special} ->
cond do
kind != :identifier or :lists.member(:at, special) ->
:not_callable
# identifier_tokenizer used to return errors for non-nfc, but
# now it nfc-normalizes everything. However, lack of nfc is
# still a good reason to quote an atom when printing.
:lists.member(:nfkc, special) ->
:other
true ->
:identifier
end
_ ->
:other
end
end
classify_binary(Atom.to_string(atom), atom)
end
end
defp valid_alias?([?E, ?l, ?i, ?x, ?i, ?r] ++ rest), do: valid_alias_piece?(rest)
defp valid_alias?(_other), do: false
# ASCII identifiers and aliases are recognized on the binary to avoid building a
# charlist and running the (unicode aware) tokenizer, which dominates the cost of
# classifying keyword list, map and struct keys.
defp classify_binary(<<char, rest::binary>>, atom)
when char >= ?a and char <= ?z
when char == ?_ do
if valid_identifier_rest?(rest), do: :identifier, else: classify_with_tokenizer(atom)
end
defp valid_alias_piece?([?., char | rest]) when char >= ?A and char <= ?Z,
do: valid_alias_piece?(trim_leading_while_valid_identifier(rest))
defp classify_binary("Elixir" <> rest, atom) do
if valid_alias_piece?(rest), do: :alias, else: classify_with_tokenizer(atom)
end
defp valid_alias_piece?([]), do: true
defp valid_alias_piece?(_other), do: false
defp classify_binary(_binary, atom), do: classify_with_tokenizer(atom)
defp trim_leading_while_valid_identifier([char | rest])
defp classify_with_tokenizer(atom) do
case :elixir_config.identifier_tokenizer().tokenize(Atom.to_charlist(atom)) do
{kind, _acc, [], _, _, special} ->
cond do
kind != :identifier or :lists.member(:at, special) ->
:not_callable
# identifier_tokenizer used to return errors for non-nfc, but
# now it nfc-normalizes everything. However, lack of nfc is
# still a good reason to quote an atom when printing.
:lists.member(:nfkc, special) ->
:other
true ->
:identifier
end
_ ->
:other
end
end
defp valid_identifier_rest?(<<char, rest::binary>>)
when char >= ?a and char <= ?z
when char >= ?A and char <= ?Z
when char >= ?0 and char <= ?9
when char == ?_ do
trim_leading_while_valid_identifier(rest)
valid_identifier_rest?(rest)
end
defp trim_leading_while_valid_identifier(other) do
other
defp valid_identifier_rest?(<<char>>) when char == ?? when char == ?!, do: true
defp valid_identifier_rest?(<<>>), do: true
defp valid_identifier_rest?(_other), do: false
defp valid_alias_piece?(<<?., char, rest::binary>>) when char >= ?A and char <= ?Z,
do: valid_alias_piece_rest?(rest)
defp valid_alias_piece?(<<>>), do: true
defp valid_alias_piece?(_other), do: false
# A helper returning the rest of the binary would build a sub binary per piece,
# so branch back into valid_alias_piece?/1 to keep the match context.
defp valid_alias_piece_rest?(<<char, rest::binary>>)
when char >= ?a and char <= ?z
when char >= ?A and char <= ?Z
when char >= ?0 and char <= ?9
when char == ?_ do
valid_alias_piece_rest?(rest)
end
defp valid_alias_piece_rest?(other), do: valid_alias_piece?(other)
@doc """
Default backend for `Kernel.dbg/2`.
@@ -2693,7 +2791,7 @@ defmodule Macro do
:guard ->
raise ArgumentError,
"invalid expression in guard, dbg is not allowed in guards. " <>
"To learn more about guards, visit: https://hexdocs.pm/elixir/patterns-and-guards.html"
"To learn more about guards, visit: https://elixir.hexdocs.pm/patterns-and-guards.html"
_ ->
:ok
@@ -2948,6 +3046,8 @@ defmodule Macro do
@doc false
def __dbg__(to_debug, header, options) do
{print_location?, options} = Keyword.pop(options, :print_location, true)
syntax_colors = if IO.ANSI.enabled?(), do: IO.ANSI.syntax_colors(), else: []
options = Keyword.merge([width: 80, pretty: true, syntax_colors: syntax_colors], options)
ansi_enabled? = options[:syntax_colors] != []
if print_location? and is_binary(header) do
@@ -2955,8 +3055,6 @@ defmodule Macro do
:ok = IO.write(IO.ANSI.format(formatted, ansi_enabled?))
end
syntax_colors = if IO.ANSI.enabled?(), do: IO.ANSI.syntax_colors(), else: []
options = Keyword.merge([width: 80, pretty: true, syntax_colors: syntax_colors], options)
{formatted, result} = dbg_format_ast_to_debug(to_debug, options)
:ok = IO.write(IO.ANSI.format([formatted, ?\n], ansi_enabled?))
result
+14 -13
View File
@@ -203,23 +203,23 @@ defmodule Macro.Env do
Returns a keyword list containing the file and line
information as keys.
"""
@spec location(t) :: keyword
@spec location(t) :: [file: file, line: line]
def location(env)
def location(%{__struct__: Macro.Env, file: file, line: line}) do
[file: file, line: line]
end
# TODO: Deprecate on Elixir 1.21 in favor of expand_alias/4
@doc false
@deprecated "Use Macro.Env.expand_alias/4 instead"
def fetch_alias(%{__struct__: Macro.Env, aliases: aliases}, atom) when is_atom(atom),
do: Keyword.fetch(aliases, :"Elixir.#{atom}")
do: Keyword.fetch(aliases, String.to_unsafe_atom("Elixir.#{atom}"))
# TODO: Deprecate on Elixir 1.21 in favor of expand_alias/4
@doc false
@deprecated "Use Macro.Env.expand_alias/4 instead"
def fetch_macro_alias(%{__struct__: Macro.Env, macro_aliases: aliases}, atom)
when is_atom(atom),
do: Keyword.fetch(aliases, :"Elixir.#{atom}")
do: Keyword.fetch(aliases, String.to_unsafe_atom("Elixir.#{atom}"))
@doc """
Returns the modules from which the given `{name, arity}` was
@@ -234,7 +234,7 @@ defmodule Macro.Env do
> This function does not emit compiler tracing events,
> which may block the compiler from correctly tracking
> dependencies. Use this function for reflection purposes
> but to do not use it to expand imports into qualified
> but do not use it to expand imports into qualified
> calls. Instead, use `expand_import/5`.
## Examples
@@ -345,7 +345,7 @@ defmodule Macro.Env do
## Additional options
It accepts the same options as `Kernel.SpecialForm.require/2` plus:
It accepts the same options as `Kernel.SpecialForms.require/2` plus:
* #{trace_option}
@@ -367,7 +367,8 @@ defmodule Macro.Env do
"""
@doc since: "1.17.0"
@spec define_require(t, Macro.metadata(), module, define_require_opts) :: {:ok, t}
@spec define_require(t, Macro.metadata(), module, define_require_opts) ::
{:ok, t} | {:error, String.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)
@@ -385,7 +386,7 @@ defmodule Macro.Env do
## Additional options
It accepts the same options as `Kernel.SpecialForm.import/2` plus:
It accepts the same options as `Kernel.SpecialForms.import/2` plus:
* `:emit_warnings` - emit warnings found when defining imports
@@ -393,7 +394,7 @@ defmodule Macro.Env do
* `:info_callback` - a function to use instead of `c:Module.__info__/1`.
The function will be invoked with `:functions` or `:macros` argument.
It has to return a list of `{function, arity}` key value pairs.
It has to return a list of `{function, arity}` key-value pairs.
If it fails, it defaults to using module metadata based on `module_info/1`.
## Examples
@@ -405,7 +406,7 @@ defmodule Macro.Env do
iex> Macro.Env.lookup_import(env, {:flatten, 1})
[{:function, List}]
It accepts the same options as `Kernel.SpecialForm.import/2`:
It accepts the same options as `Kernel.SpecialForms.import/2`:
iex> env = __ENV__
iex> Macro.Env.lookup_import(env, {:is_odd, 1})
@@ -447,7 +448,7 @@ defmodule Macro.Env do
## Additional options
It accepts the same options as `Kernel.SpecialForm.alias/2` plus:
It accepts the same options as `Kernel.SpecialForms.alias/2` plus:
* #{trace_option}
@@ -699,7 +700,7 @@ defmodule Macro.Env do
@doc """
Returns the environment stacktrace.
"""
@spec stacktrace(t) :: list
@spec stacktrace(t) :: [{module, atom, arity, keyword}]
def stacktrace(%{__struct__: Macro.Env} = env) do
cond do
is_nil(env.module) ->
+14 -5
View File
@@ -201,7 +201,7 @@ defmodule Map do
%{}
"""
@spec new :: map
@spec new :: %{}
def new, do: %{}
@doc """
@@ -547,6 +547,8 @@ defmodule Map do
:erlang.error({:badmap, non_map})
end
defp take([], _map, []), do: %{}
defp take([], _map, acc) do
:maps.from_list(acc)
end
@@ -1107,7 +1109,7 @@ defmodule Map do
iex> Map.equal?(%{a: 1, b: 2}, %{b: 1, a: 2})
false
Comparison between keys and values is done with `===/3`,
Comparison between keys and values is done with `===/2`,
which means integers are not equivalent to floats:
iex> Map.equal?(%{a: 1.0}, %{a: 1})
@@ -1136,9 +1138,9 @@ defmodule Map do
> #### Performance considerations {: .tip}
>
> If you find yourself doing multiple calls to `Map.filter/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`.
> 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
@@ -1170,6 +1172,13 @@ 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)
+25 -16
View File
@@ -51,11 +51,24 @@ defmodule MapSet do
that they share many properties, including logarithmic time complexity. Erlang
`:sets` (version 2) are implemented on top of maps, so see the documentation
for `Map` for more information on its execution time complexity.
> #### Dialyzer opaqueness warnings {: .warning}
>
> `MapSet` internally relies on the `:sets` module which uses
> opaque types. This might cause Dialyzer to report opaqueness violations.
> These can be silenced by setting the following module attribute:
>
> @dialyzer :no_opaque
"""
@type value :: term
@opaque internal(value) :: :sets.set(value)
# 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()
@type t(value) :: %__MODULE__{map: internal(value)}
@type t :: t(term)
@@ -167,18 +180,14 @@ defmodule MapSet do
@doc since: "1.14.0"
@spec symmetric_difference(t(val1), t(val2)) :: t(val1 | val2) when val1: value, val2: value
def symmetric_difference(%MapSet{map: set1} = map_set1, %MapSet{map: set2} = _map_set2) do
{small, large} = if :sets.size(set1) <= :sets.size(set2), do: {set1, set2}, else: {set2, set1}
disjointer_fun = fn elem, {small, acc} ->
if :sets.is_element(elem, small) do
{:sets.del_element(elem, small), acc}
map =
if :sets.is_disjoint(set1, set2) do
:sets.union(set1, set2)
else
{small, [elem | acc]}
:sets.union(:sets.subtract(set1, set2), :sets.subtract(set2, set1))
end
end
{new_small, list} = :sets.fold(disjointer_fun, {small, []}, large)
%{map_set1 | map: :sets.union(new_small, :sets.from_list(list, version: 2))}
%{map_set1 | map: map}
end
@doc """
@@ -201,7 +210,7 @@ defmodule MapSet do
Checks if two sets are equal.
The comparison between elements is done using `===/2`,
which a set with `1` is not equivalent to a set with
which means a set with `1` is not equivalent to a set with
`1.0`.
## Examples
@@ -340,7 +349,7 @@ defmodule MapSet do
> If you find yourself doing multiple calls to `MapSet.filter/2`
> and `MapSet.reject/2` in a pipeline, it is likely more efficient
> to use `Enum.map/2` and `Enum.filter/2` instead and convert to
> a map at the end using `MapSet.new/1`.
> a set at the end using `MapSet.new/1`.
## Examples
@@ -396,15 +405,15 @@ defmodule MapSet do
iex> while_false
MapSet.new([1, 3])
iex> {while_true, while_false} = MapSet.split_with(MapSet.new(), fn {_k, v} -> v > 50 end)
iex> {while_true, while_false} = MapSet.split_with(MapSet.new([10, 20, 60, 70]), fn v -> v > 50 end)
iex> while_true
MapSet.new([])
MapSet.new([60, 70])
iex> while_false
MapSet.new([])
MapSet.new([10, 20])
"""
@doc since: "1.15.0"
@spec split_with(MapSet.t(), (term() -> as_boolean(term))) :: {MapSet.t(), MapSet.t()}
@spec split_with(t(a), (a -> as_boolean(term))) :: {t(a), t(a)} when a: value
def split_with(%MapSet{map: map}, fun) when is_function(fun, 1) do
{while_true, while_false} = Map.split_with(map, fn {key, _} -> fun.(key) end)
{%MapSet{map: while_true}, %MapSet{map: while_false}}
+58 -46
View File
@@ -190,7 +190,7 @@ defmodule Module do
* a string (often a heredoc)
* `false`, which will make the entity invisible to documentation-extraction
tools like [`ExDoc`](https://hexdocs.pm/ex_doc/)
tools like [`ExDoc`](https://ex-doc.hexdocs.pm/)
* a keyword list, since Elixir 1.7.0
For example:
@@ -216,7 +216,7 @@ defmodule Module do
As can be seen in the example above, since Elixir 1.7.0 `@doc` and `@typedoc`
also accept a keyword list that serves as a way to provide arbitrary metadata
about the entity. Tools like [`ExDoc`](https://hexdocs.pm/ex_doc/) and
about the entity. Tools like [`ExDoc`](https://ex-doc.hexdocs.pm/) and
`IEx` may use this information to display annotations. A common use
case is the `:since` key, which may be used to annotate in which version the
function was introduced.
@@ -263,7 +263,7 @@ defmodule Module do
Tools may use this information to ensure the module is recompiled
in case any of the external resources change, see for example:
[`mix compile.elixir`](https://hexdocs.pm/mix/Mix.Tasks.Compile.Elixir.html).
[`mix compile.elixir`](https://mix.hexdocs.pm/Mix.Tasks.Compile.Elixir.html).
The specified file path provided is interpreted as relative to
the folder containing the project's `mix.exs`, which is the
@@ -321,7 +321,7 @@ defmodule Module do
Accepts a string (often a heredoc) or `false` where `@moduledoc false`
will make the module invisible to documentation extraction tools like
[`ExDoc`](https://hexdocs.pm/ex_doc/).
[`ExDoc`](https://ex-doc.hexdocs.pm/).
Similarly to `@doc` also accepts a keyword list to provide metadata
about the module. For more details, see the documentation of `@doc`
@@ -366,7 +366,8 @@ defmodule Module do
Unlike other hooks, `@on_definition` will only invoke functions and
never macros. This is to avoid `@on_definition` callbacks from
redefining functions that have just been defined in favor of more
explicit approaches.
explicit approaches. They are also invoked in the reverse order of
registration.
When just a module is provided, the function is assumed to be
`__on_definition__/6`.
@@ -656,7 +657,8 @@ defmodule Module do
@spec module_info(:attributes) :: keyword()
@spec module_info(:compile) :: keyword()
@spec module_info(:md5) :: binary()
@spec module_info(:nifs) :: module()
@spec module_info(:nifs) :: [function_info]
when function_info: {function_name :: atom(), arity :: non_neg_integer()}
@spec module_info(:exports) :: [function_info]
when function_info: {function_name :: atom(), arity :: non_neg_integer()}
@spec module_info(:functions) :: [function_info]
@@ -679,7 +681,7 @@ defmodule Module do
information. For documentation, see `c:Module.__info__/1`.
'''
@type definition :: {atom, arity}
@type definition :: {function_name :: atom, arity}
@type def_kind :: :def | :defp | :defmacro | :defmacrop
@type create_opts :: [
@@ -752,7 +754,7 @@ defmodule Module do
"""
@doc since: "1.12.0"
@spec reserved_attributes() :: map
@spec reserved_attributes() :: %{optional(atom()) => %{doc: binary()}}
def reserved_attributes() do
%{
after_compile: %{
@@ -958,7 +960,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
@@ -984,7 +986,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
@@ -1015,7 +1017,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
@@ -1039,7 +1041,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
@@ -1146,7 +1148,7 @@ defmodule Module do
defp simplify_var(var, guess_priority) do
case Atom.to_string(var) do
"_" -> {:_, [], guess_priority}
"_" <> rest -> {String.to_atom(rest), [], guess_priority}
"_" <> rest -> {String.to_unsafe_atom(rest), [], guess_priority}
_ -> {var, [], nil}
end
end
@@ -1157,7 +1159,7 @@ defmodule Module do
rescue
ArgumentError -> module
else
module_name -> String.to_atom(Macro.underscore(List.last(module_name)))
module_name -> String.to_unsafe_atom(Macro.underscore(List.last(module_name)))
end
end
@@ -1190,7 +1192,7 @@ defmodule Module do
defp expand_key(key, counters) do
case counters do
%{^key => count} when is_integer(count) and count >= 1 ->
{{:"#{key}#{count}", [], Elixir}, Map.put(counters, key, count - 1)}
{{String.to_unsafe_atom("#{key}#{count}"), [], Elixir}, Map.put(counters, key, count - 1)}
_ ->
{{key, [], Elixir}, counters}
@@ -1223,7 +1225,8 @@ defmodule Module do
defp merge_signature({var, _, _} = older, {var, _, _}, _), do: older
# Otherwise, returns a generic guess
defp merge_signature({_, meta, _}, _newer, i), do: {:"arg#{i}", meta, Elixir}
defp merge_signature({_, meta, _}, _newer, i),
do: {String.to_unsafe_atom("arg#{i}"), meta, Elixir}
@doc """
Checks if the module defines the given function or macro.
@@ -1249,17 +1252,18 @@ defmodule Module do
"""
@spec defines?(module, definition) :: boolean
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
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
{set, _bag} = data_tables_for!(module, __ENV__.function, @extra_error_msg_defines?)
:ets.member(set, {:def, tuple})
:ets.member(set, {:def, definition})
end
@doc """
Checks if the module defines a function or macro of the
given `kind`.
given kind.
`kind` can be any of `:def`, `:defp`, `:defmacro`, or `:defmacrop`.
`def_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
@@ -1275,12 +1279,13 @@ defmodule Module do
"""
@spec defines?(module, definition, def_kind) :: boolean
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 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_kind in [:def, :defp, :defmacro, :defmacrop] do
{set, _bag} = data_tables_for!(module, __ENV__.function, @extra_error_msg_defines?)
case :ets.lookup(set, {:def, tuple}) do
case :ets.lookup(set, {:def, definition}) do
[{_, ^def_kind, _, _, _, _}] -> true
_ -> false
end
@@ -1293,7 +1298,9 @@ defmodule Module do
"""
@doc since: "1.7.0"
@spec defines_type?(module, definition) :: boolean
def defines_type?(module, definition) when is_atom(module) do
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
Kernel.Typespec.defines_type?(module, definition)
end
@@ -1306,7 +1313,9 @@ defmodule Module do
"""
@doc since: "1.7.0"
@spec spec_to_callback(module, definition) :: boolean
def spec_to_callback(module, definition) do
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
Kernel.Typespec.spec_to_callback(module, definition)
end
@@ -1339,7 +1348,7 @@ defmodule Module do
@doc """
Returns all overridable definitions in `module`.
Note a definition is included even if it was was already overridden.
Note a definition is included even if it 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.
@@ -1358,7 +1367,7 @@ defmodule Module do
"""
@doc since: "1.13.0"
@spec overridables_in(module) :: [atom]
@spec overridables_in(module) :: [definition]
def overridables_in(module) when is_atom(module) do
assert_not_compiled!(__ENV__.function, module, :all)
:elixir_overridable.overridables_for(module)
@@ -1407,10 +1416,10 @@ defmodule Module do
"""
@spec definitions_in(module, def_kind) :: [definition]
def definitions_in(module, kind)
when is_atom(module) and kind in [:def, :defp, :defmacro, :defmacrop] do
def definitions_in(module, def_kind)
when is_atom(module) and def_kind in [:def, :defp, :defmacro, :defmacrop] do
{set, _} = data_tables_for!(module, __ENV__.function, @extra_error_msg_definitions_in)
:ets.select(set, [{{{:def, :"$1"}, kind, :_, :_, :_, :_}, [], [:"$1"]}])
:ets.select(set, [{{{:def, :"$1"}, def_kind, :_, :_, :_, :_}, [], [:"$1"]}])
end
@doc """
@@ -1441,16 +1450,17 @@ defmodule Module do
[{meta :: keyword, arguments :: [Macro.t()], guards :: [Macro.t()], Macro.t()}]}
| nil
@doc since: "1.12.0"
def get_definition(module, {name, arity}, options \\ [])
when is_atom(module) and is_atom(name) and is_integer(arity) and is_list(options) do
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
{set, bag} = data_tables_for!(module, __ENV__.function, "")
case :ets.lookup(set, {:def, {name, arity}}) do
case :ets.lookup(set, {:def, {function_name, arity}}) do
[{_key, kind, meta, _, _, _}] ->
clauses =
if options[:skip_clauses],
do: [],
else: bag_lookup_element(bag, {:clauses, {name, arity}}, 2)
else: bag_lookup_element(bag, {:clauses, {function_name, arity}}, 2)
{:v1, kind, meta, clauses}
@@ -1467,10 +1477,11 @@ defmodule Module do
"""
@doc since: "1.12.0"
@spec delete_definition(module, definition) :: boolean()
def delete_definition(module, {name, arity})
when is_atom(module) and is_atom(name) and is_integer(arity) do
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
assert_not_compiled!(__ENV__.function, module, :writeable)
:elixir_def.take_definition(module, {name, arity}) != false
:elixir_def.take_definition(module, {function_name, arity}) != false
end
@doc """
@@ -1486,20 +1497,20 @@ defmodule Module do
given.
"""
@spec make_overridable(module, [definition]) :: :ok
def make_overridable(module, tuples) when is_atom(module) and is_list(tuples) do
def make_overridable(module, definitions) when is_atom(module) and is_list(definitions) do
assert_not_compiled!(__ENV__.function, module, :writeable)
func = fn
{function_name, arity} = tuple
{function_name, arity} = definition
when is_atom(function_name) and is_integer(arity) and arity >= 0 and arity <= 255 ->
case :elixir_def.take_definition(module, tuple) do
case :elixir_def.take_definition(module, definition) do
false ->
raise ArgumentError,
"cannot make function #{function_name}/#{arity} " <>
"overridable because it was not defined"
clause ->
:elixir_overridable.record_overridable(module, tuple, clause)
:elixir_overridable.record_overridable(module, definition, clause)
end
other ->
@@ -1508,7 +1519,7 @@ defmodule Module do
"{function_name :: atom, arity :: 0..255} tuple, got: #{inspect(other)}"
end
:lists.foreach(func, tuples)
:lists.foreach(func, definitions)
end
@spec make_overridable(module, module) :: :ok
@@ -1566,9 +1577,10 @@ defmodule Module do
exists or one is pending.
"""
@spec overridable?(module, definition) :: boolean
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
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
end
@doc """
+1 -5
View File
@@ -228,11 +228,7 @@ defmodule Module.Behaviour do
defp behaviour_defined?(callbacks, behaviour) do
callbacks
|> Map.values()
|> List.flatten()
|> Enum.any?(fn
{_kind, ^behaviour, _optional?} -> true
{_kind, _behaviour, _optional?} -> false
end)
|> Enum.any?(fn list -> :lists.keymember(behaviour, 2, list) end)
end
defp warn_missing_impls(%{callbacks: callbacks} = context, _impl_contexts, _defs)
+164 -101
View File
@@ -10,6 +10,7 @@ defmodule Module.ParallelChecker do
@type cache() :: {pid(), :ets.tid()}
@type warning() :: term()
@type error() :: term()
@type mode() :: :erlang | :elixir | :protocol
@typedoc """
@@ -19,6 +20,7 @@ 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()}
]
@@ -66,17 +68,37 @@ 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.
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
inner_spawn(pid, checker, table, module, info, log?)
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 inner_spawn(pid, checker, table, module, info, log?) do
defp spawn_checker(ref, pid, checker, table, module, module_tuple, log?) do
spawn(fn ->
mon_ref = Process.monitor(pid)
receive do
{^ref, :check, profile} ->
# Set the compiler info so we can collect warnings
Process.link(pid)
:erlang.put(:elixir_compiler_info, {pid, self()})
{warnings, errors} = check_module(module_tuple, {checker, table}, log?, profile)
send(pid, {__MODULE__, module, warnings, errors})
send(checker, {__MODULE__, :checked, ref})
{:DOWN, ^mon_ref, _, _, _} ->
:ok
end
end)
end
defp spawn_and_register_cache(pid, checker, table, module, info, log?) do
ref = make_ref()
spawned =
@@ -87,48 +109,28 @@ defmodule Module.ParallelChecker do
{^ref, :cache} ->
Process.link(pid)
{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
location =
case :code.which(module) do
[_ | _] = path -> path
_ -> info
end
# 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})
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, [], []})
end
{:DOWN, ^mon_ref, _, _, _} ->
@@ -136,7 +138,7 @@ defmodule Module.ParallelChecker do
end
end)
register(checker, module, spawned, ref)
register_cache(checker, module, spawned, ref)
:ok
end
@@ -184,39 +186,44 @@ defmodule Module.ParallelChecker do
Returns the updated list of warnings from the verification.
"""
@spec verify(cache(), [{module(), Path.t()}]) :: [warning()]
@spec verify(cache(), [{module(), Path.t()}]) :: {[warning()], [error()]}
def verify({checker, table}, runtime_files) do
value = :erlang.get(:elixir_code_diagnostics)
log? = not match?({_, false}, value)
for {module, file} <- runtime_files do
inner_spawn(self(), checker, table, module, file, log?)
spawn_and_register_cache(self(), checker, table, module, file, log?)
end
count = :gen_server.call(checker, :start, :infinity)
diagnostics = collect_results(count, [])
{warnings, errors} = collect_results(count, [], [])
case :erlang.get(:elixir_code_diagnostics) do
:undefined -> :ok
{tail, log?} -> :erlang.put(:elixir_code_diagnostics, {diagnostics ++ tail, log?})
{tail, log?} -> :erlang.put(:elixir_code_diagnostics, {errors ++ warnings ++ tail, log?})
end
diagnostics
{warnings, errors}
end
defp collect_results(0, diagnostics) do
diagnostics
defp collect_results(0, warnings, errors) do
{warnings, errors}
end
defp collect_results(count, diagnostics) do
defp collect_results(count, warnings, errors) 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])
{__MODULE__, _module, new_diagnostics} ->
collect_results(count - 1, new_diagnostics ++ 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)
end
end
@@ -256,7 +263,7 @@ defmodule Module.ParallelChecker do
## Module checking
defp check_module(module_tuple, cache, log?) do
defp check_module(module_tuple, cache, log?, profile) do
{module, file, line, definitions, no_warn_undefined, behaviours, impls, attrs, after_verify} =
module_tuple
@@ -270,18 +277,23 @@ defmodule Module.ParallelChecker do
definitions
)
diagnostics =
module
|> Module.Types.warnings(file, attrs, definitions, no_warn_undefined, cache)
type_warnings =
profile(module, profile, fn ->
Module.Types.warnings(module, file, attrs, definitions, no_warn_undefined, cache)
end)
{warnings, errors} =
type_warnings
|> Kernel.++(behaviour_warnings)
|> group_warnings()
|> emit_warnings(file, log?)
|> group_diagnostics()
|> emit_diagnostics(file, log?)
|> Enum.split_with(&(&1.severity == :warning))
Enum.each(after_verify, fn {verify_mod, verify_fun} ->
apply(verify_mod, verify_fun, [module])
end)
diagnostics
{warnings, errors}
end
defp module_map_to_module_tuple(module_map) do
@@ -332,16 +344,16 @@ defmodule Module.ParallelChecker do
## Warning helpers
defp group_warnings(warnings) do
defp group_diagnostics(triplets) do
{ungrouped, grouped} =
Enum.reduce(warnings, {[], %{}}, fn {module, warning, location}, {ungrouped, grouped} ->
%{message: _} = diagnostic = module.format_diagnostic(warning)
Enum.reduce(triplets, {[], %{}}, fn {module, term, location}, {ungrouped, grouped} ->
%{message: _} = diagnostic = module.format_diagnostic(term)
if Map.get(diagnostic, :group, false) do
locations = MapSet.new([location])
grouped =
Map.update(grouped, warning, {locations, diagnostic}, fn
Map.update(grouped, term, {locations, diagnostic}, fn
{locations, diagnostic} -> {MapSet.put(locations, location), diagnostic}
end)
@@ -359,7 +371,7 @@ defmodule Module.ParallelChecker do
Enum.sort(ungrouped ++ grouped)
end
defp emit_warnings(warnings, file, log?) do
defp emit_diagnostics(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)
@@ -521,8 +533,12 @@ defmodule Module.ParallelChecker do
:gen_server.call(server, {:unlock, module, mode}, :infinity)
end
defp register(server, module, pid, ref) do
:gen_server.cast(server, {:register, module, pid, ref})
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})
end
## Server callbacks
@@ -565,11 +581,13 @@ defmodule Module.ParallelChecker do
state = %{
waiting: %{},
modules: [],
caches: [],
checkers: [],
spawned: %{},
schedulers: schedulers,
threshold: threshold,
callback: callback,
profile: Keyword.get(options, :profile),
protocols: [],
table: table
}
@@ -577,20 +595,9 @@ defmodule Module.ParallelChecker do
:gen_server.enter_loop(__MODULE__, [], state)
end
def handle_call(:start, _from, %{modules: modules, protocols: protocols, table: table} = state) do
def handle_call(:start, _from, %{caches: caches, protocols: protocols, table: table} = state) do
:ets.insert(table, Enum.map(protocols, &{&1, :uncached}))
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: []})}
{:reply, length(caches), run_caches(%{state | protocols: []})}
end
def handle_call({:lock, module}, from, %{waiting: waiting} = state) do
@@ -620,10 +627,21 @@ defmodule Module.ParallelChecker do
{:noreply, state}
end
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)
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)
timer && Process.cancel_timer(timer)
{:noreply, run_checkers(%{state | spawned: spawned})}
end
@@ -632,11 +650,41 @@ defmodule Module.ParallelChecker do
{:stop, :normal, state}
end
def handle_cast({:register, module, pid, ref}, %{modules: modules} = state) do
{:noreply, %{state | modules: [{module, pid, ref} | modules]}}
def handle_cast({:register_cache, module, pid, ref}, %{caches: caches} = state) do
{:noreply, %{state | caches: [{module, pid, ref} | caches]}}
end
defp run_checkers(%{modules: []} = state) do
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
state
end
@@ -645,10 +693,25 @@ defmodule Module.ParallelChecker do
state
end
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})
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.()
end
end
+299 -52
View File
@@ -4,7 +4,7 @@
defmodule Module.Types do
@moduledoc false
alias Module.Types.{Descr, Expr, Pattern, Helpers}
alias Module.Types.{Apply, Descr, Expr, Helpers, Pattern}
# The mode controls what happens on function application when
# there are gradual arguments. Non-gradual arguments always
@@ -86,7 +86,7 @@ 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, inferred} | types], private, context}
{[{fun_arity, group_clauses_by_return(inferred)} | types], private, context}
else
{types, private, context}
end
@@ -136,8 +136,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, :behaviour_info, 1) do
{for, protocol.behaviour_info(:callbacks)}
if Code.ensure_loaded?(protocol) and function_exported?(protocol, :__protocol__, 1) do
{for, protocol.__protocol__(:functions)}
else
nil
end
@@ -178,7 +178,7 @@ defmodule Module.Types do
default = Keyword.get(meta, :defaults, 0)
cond do
Keyword.get(meta, :context) != nil ->
Keyword.get(meta, :context) != nil or Keyword.get(meta, :from_super) == true ->
:ok
default == 0 ->
@@ -232,7 +232,7 @@ defmodule Module.Types do
context =
Enum.reduce(defs, context(), fn {fun_arity, _kind, meta, _clauses} = def, context ->
# Optimized version of finder, since we already the definition
# Optimized version of finder, since we already have the definition
finder = fn _ -> default_domain(:dynamic, def, fun_arity, impl) end
{_kind, _inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
context
@@ -251,15 +251,21 @@ defmodule Module.Types do
context ->
{_kind, info, mapping} = Map.fetch!(context.local_sigs, fun_arity)
clauses_indexes =
for type_index <- pending,
not skip_unused_clause?(info, type_index),
{clause_index, ^type_index} <- mapping,
do: clause_index
{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)
Enum.reduce(clauses_indexes, context, fn clause_index, context ->
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}
stack = %{stack | function: fun_arity} |> with_file_meta(meta)
Helpers.warn(__MODULE__, {:unused_clause, kind, fun_arity}, meta, stack, context)
end)
end
@@ -289,11 +295,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, meta, clauses, expected, stack, context)
local_handler(mode, fun_arity, kind, clauses, expected, stack, context)
context =
update_in(context.local_sigs, &Map.put(&1, fun_arity, {kind, inferred, mapping}))
@@ -306,9 +312,10 @@ defmodule Module.Types do
end
end
defp local_handler(:traverse, {_, arity}, _kind, _meta, clauses, _expected, stack, context) do
defp local_handler(:traverse, {_, arity}, _kind, clauses, _expected, stack, context) do
context =
Enum.reduce(clauses, context, fn {_meta, _args, _guards, body}, 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)
@@ -316,57 +323,234 @@ defmodule Module.Types do
{inferred, [{0, 0}], context}
end
defp local_handler(mode, fun_arity, kind, meta, clauses, expected, stack, context) do
defp local_handler(mode, fun_arity, kind, clauses, expected, stack, context) do
{fun, _arity} = fun_arity
stack = stack |> fresh_stack(mode, fun_arity) |> with_file_meta(meta)
stack = fresh_stack(stack, mode, fun_arity)
base_info = {:def, kind, fun, expected}
{_, _, 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)
case clauses do
[{meta, args, [], {:super, _, [_ | _]} = body}] ->
default_local_handler(meta, args, body, base_info, kind, fun, expected, stack, context)
try do
{trees, context} =
Pattern.of_head(args, guards, expected, {:infer, expected}, meta, stack, context)
_ ->
infer_local_handler(clauses, base_info, kind, fun, expected, stack, context)
end
end
{return_type, context} =
Expr.of_expr(body, Descr.term(), body, stack, context)
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}
args_types = Pattern.of_domain(trees, context)
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, args_types, return_type, total - 1, [])
add_inferred(inferred, clause_types, return_type, total - 1, [])
if type_index == -1 do
{index + 1, total + 1, [{index, total} | mapping], inferred, context}
else
{index + 1, total, [{index, type_index} | mapping], inferred, context}
end
total = if type_index == -1, do: total + 1, else: total
{index + 1, total, [{0, index} | mapping], inferred}
end)
domain =
case inferred do
[_] ->
nil
_ ->
inferred
|> Enum.map(fn {args, _} -> args end)
|> Enum.zip_with(fn types -> Enum.reduce(types, &Descr.opt_union/2) end)
end
{{:infer, domain, Enum.reverse(inferred)}, mapping, restore_context(body_context, context)}
rescue
e ->
internal_error!(e, __STACKTRACE__, kind, meta, fun, args, guards, body, stack)
end
end
defp infer_local_handler(clauses, base_info, kind, fun, expected, stack, context) do
{_, clauses_types, clauses_context} =
Enum.reduce(clauses, {Pattern.init_previous(), [], context}, fn
{meta, args, guards, body}, {previous, inferred, acc_context} ->
stack = with_file_meta(stack, meta)
fresh_context = fresh_context(acc_context)
info = {base_info, args, guards}
try do
{trees, precise?, head_no_previous_args_types, previous, head_context} =
Pattern.of_head(args, guards, expected, previous, info, meta, stack, fresh_context)
{return_type, context} =
Expr.of_expr(body, Descr.term(), body, stack, head_context)
args_types = Pattern.of_domain(trees, stack, context)
head_args_types =
case inferred do
[] -> nil
_ -> Pattern.of_domain(trees, stack, head_context)
end
args_triplet = {args_types, head_args_types, head_no_previous_args_types}
inferred = [{args_triplet, return_type, precise?} | inferred]
{previous, inferred, context}
rescue
e ->
internal_error!(e, __STACKTRACE__, kind, meta, fun, args, guards, body, stack)
end
end)
{clauses_types, mapping, domain} =
clauses_types
|> Enum.reverse()
|> group_clauses()
domain =
case clauses_types do
[_] ->
nil
_ ->
clauses_types
|> Enum.map(fn {args, _} -> args end)
|> Enum.zip_with(fn types -> Enum.reduce(types, &Descr.union/2) end)
[_] -> nil
_ -> domain
end
inferred = {:infer, domain, Enum.reverse(clauses_types)}
inferred = {:infer, domain, clauses_types}
{inferred, mapping, restore_context(clauses_context, context)}
end
defp group_clauses(clauses) do
{_, all_clauses, filtered_clauses, non_empty?} =
Enum.reduce(clauses, {0, [], [], false}, fn
{_args_triplet, return, precise?} = clause,
{index, all_clauses, filtered_clauses, non_empty?} ->
empty? = Descr.empty?(return)
indexed_clause = {clause, index}
filtered_clauses =
if precise? and empty? do
filtered_clauses
else
[indexed_clause | filtered_clauses]
end
{index + 1, [indexed_clause | all_clauses], filtered_clauses, non_empty? or not empty?}
end)
clauses =
if non_empty? do
Enum.reverse(filtered_clauses)
else
Enum.reverse(all_clauses)
end
[
{{{args, _head_args, _head_no_previous_args}, _return, _precise?}, _index}
| clauses_tail
] = clauses
domain =
Enum.reduce(clauses_tail, args, fn
{{{args, head_args, head_no_previous_args}, _return, _precise?}, _index}, domain ->
compute_domain(args, head_args, head_no_previous_args, domain)
end)
{_, mapping, inferred} =
Enum.reduce(clauses, {0, [], []}, fn
{{{args, _head_args, _head_no_previous_args}, return, _precise?}, index},
{total, mapping, inferred} ->
{type_index, inferred} = add_inferred(inferred, args, return, total - 1, [])
if type_index == -1 do
{total + 1, [{index, total} | mapping], inferred}
else
{total, [{index, type_index} | mapping], inferred}
end
end)
{Enum.reverse(inferred), mapping, domain}
end
defp compute_domain(
[arg | args_types],
[head_arg | head_args_types],
[no_prev_arg | no_prev_args_types],
[d | domain]
) do
[
# This is an optimization that broadens the domain, but it is acceptable
# because the domain is used for reverse arrows and not type checking.
#
# The overall idea is that, if we have a function with three clauses,
# the domain is computed by unioning their inferred types. However, their
# inferred types often have the different of the previous clauses:
#
# opt_union(r3 ^ (c3 - c2 - c1), r2 ^ (c2 - c1), r1 ^ c1)
#
# Where `rN` represents the refinement in every function body.
#
# What this function does is, if the type of a given arg in a clause
# before and after the body is the same (meaning r3 is term), then
# we replace all of `(c3 - c2 - c1)` by just `c3`, which removes
# many of the differences in the node. However, keep in mind that,
# because `r2` may have refine `c2` in the previous clause, the domain
# may end-up being broader. Take this example:
#
# % %{..., foo: integer()} -> binary()
# def example(%{foo: var}), do: Integer.to_string(var)
#
# % %{...} and not %{..., foo: term()} -> :error
# def example(%{}), do: :error
#
# The actual domain is:
#
# %{..., foo: not_set()} or %{..., foo: integer()}
# #=> %{..., foo: if_set(integer())}
#
# But we will infer:
#
# %{...} or %{..., foo: integer()}
# #=> %{...}
#
# We lose precision but this is exactly what we want: to have simpler types.
# Furthermore, the signature used in type checking is not refined in any way,
# so type checking is still sound.
if arg == head_arg do
Descr.opt_union(Descr.upper_bound(no_prev_arg), d)
else
Descr.opt_union(arg, d)
end
| compute_domain(args_types, head_args_types, no_prev_args_types, domain)
]
end
defp compute_domain([], [], [], []), do: []
# We check for term equality of types as an optimization
# to reduce the amount of check we do at runtime.
defp add_inferred([{args, existing_return} | tail], args, return, index, acc),
do: {index, Enum.reverse(acc, [{args, Descr.union(existing_return, return)} | tail])}
do: {index, Enum.reverse(acc, [{args, Descr.opt_union(existing_return, return)} | tail])}
defp add_inferred([head | tail], args, return, index, acc),
do: add_inferred(tail, args, return, index - 1, [head | acc])
@@ -374,6 +558,58 @@ 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.opt_union(existing_arg, arg) | acc], true)
end
defp union_args([_ | _], [_ | _], _acc, true), do: nil
# In theory fully equal args are merged on add_inferred
defp union_args([], [], acc, _changed?), do: Enum.reverse(acc)
defp with_file_meta(stack, meta) do
case Keyword.fetch(meta, :file) do
{:ok, {meta_file, _}} -> %{stack | file: meta_file}
@@ -435,7 +671,9 @@ defmodule Module.Types do
# The mode to be used, see the @modes attribute
mode: mode,
# The function for handling local calls
local_handler: handler
local_handler: handler,
# Reverse arrow handling (nil | :cache | :except_none | :include_none)
reverse_arrow: nil
}
end
@@ -446,36 +684,45 @@ 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 matches and guards
# Track metadata specific to patterns and guards
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: %{}
local_used: %{},
# Cached reverse arrows
reverse_arrows: %{}
}
end
defp fresh_stack(stack, mode, function) when mode in @modes do
%{stack | mode: mode, function: function}
%{stack | mode: mode, function: function, reverse_arrow: nil}
end
defp fresh_context(context) do
%{context | vars: %{}, failed: false}
%{context | vars: %{}, failed: false, reverse_arrows: %{}}
end
defp restore_context(later_context, %{vars: vars, failed: failed}) do
%{later_context | vars: vars, failed: failed}
defp restore_context(later_context, %{
vars: vars,
failed: failed,
reverse_arrows: reverse_arrows
}) do
%{later_context | vars: vars, failed: failed, reverse_arrows: reverse_arrows}
end
## Diagnostics
def format_diagnostic({:unused_clause, kind, {fun, arity}}) do
%{
message: "this clause of #{kind} #{fun}/#{arity} is never used"
message:
"this clause of #{kind} #{fun}/#{arity} is never used (or it will always fail/warn when invoked)"
}
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
+139 -38
View File
@@ -57,7 +57,7 @@ defmodule Module.Types.Helpers do
## Warnings
@doc """
Converts an itneger into ordinal.
Converts an integer into ordinal.
"""
def integer_to_ordinal(i) do
case rem(i, 10) do
@@ -143,8 +143,9 @@ defmodule Module.Types.Helpers do
"""
def collect_traces(expr, %{vars: vars}) do
{_, versions} =
Macro.prewalk(expr, %{}, fn
{var_name, meta, var_context}, versions when is_atom(var_name) and is_atom(var_context) ->
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
version = meta[:version]
case vars do
@@ -160,9 +161,9 @@ defmodule Module.Types.Helpers do
_ ->
{:ok, versions}
end
node, versions ->
{node, versions}
else
_ -> {node, versions}
end
end)
versions
@@ -261,7 +262,8 @@ 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.
We also undo some macro expressions done by the Kernel module
and collapse complex expressions.
## Options
@@ -327,6 +329,13 @@ 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]}
@@ -352,42 +361,134 @@ defmodule Module.Types.Helpers do
{:&, amp_meta, [{:/, slash_meta, [{{:., dot_meta, [mod, fun]}, call_meta, []}, arity]}]}
{:case, meta, [expr, [do: clauses]]} = case ->
if meta[:type_check] == :expr do
case clauses do
[
{:->, _,
[
{:case, meta, [expr, [do: clauses]]} ->
case meta[:type_check] do
{:case, op} ->
case clauses do
[
{:->, _,
[
{: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]}
]}
]}
],
true
]},
{:->, _, [[{:_, _, Kernel}], false]}
]
when op == :! ->
{:!, meta, [expr]}
[
{:->, _, [[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]}
]}
]}
],
right_side
]},
{:->, _, [[{var, _, Kernel}], {var, _, Kernel}]}
]
when op == :|| ->
{:||, meta, [expr, right_side]}
_ ->
case
end
else
case
[
{:->, _,
[
[
{: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: {:..., [], []}]]}
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]]}
+272 -117
View File
@@ -11,8 +11,7 @@ defmodule Module.Types.Of do
@prefix quote(do: ...)
@suffix quote(do: ...)
@integer_or_float union(integer(), float())
@integer_or_binary union(integer(), binary())
@integer_or_float opt_union(integer(), float())
@integer integer()
@float float()
@binary binary()
@@ -32,7 +31,7 @@ defmodule Module.Types.Of do
@doc """
Marks a variable with error.
This purposedly deletes all traces of the variable,
This purposely deletes all traces of the variable,
as it is often invoked when the cause for error is elsewhere.
"""
def error_var({_, meta, _}, context) do
@@ -49,7 +48,7 @@ defmodule Module.Types.Of do
@doc """
Declares a variable.
"""
def declare_var(var, context) do
def declare_var(var, type \\ term(), context) do
{var_name, meta, var_context} = var
version = Keyword.fetch!(meta, :version)
@@ -59,7 +58,7 @@ defmodule Module.Types.Of do
vars ->
data = %{
type: term(),
type: type,
name: var_name,
context: var_context,
off_traces: [],
@@ -87,6 +86,9 @@ 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
@@ -104,22 +106,50 @@ defmodule Module.Types.Of do
context
end
if gradual?(old_type) and type not in [term(), dynamic()] and not is_map_key(data, :errored) 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)
}
case stack do
_ when type in @skip_refinement_for or is_map_key(data, :errored) ->
{old_type, context}
{new_type, %{context | vars: %{vars | version => data}}}
%{reverse_arrow: reverse_arrow} when reverse_arrow in [:except_none, :include_none] ->
new_type = opt_intersection(old_type, type)
_ ->
{old_type, context}
end
else
{old_type, context}
case empty?(new_type) do
true when reverse_arrow == :include_none ->
data = %{
data
| type: none(),
off_traces: new_trace(expr, none(), stack, off_traces)
}
{none(), %{context | vars: %{vars | version => data}}}
false when new_type != old_type ->
data = %{
data
| type: new_type,
off_traces: new_trace(expr, new_type, stack, off_traces)
}
{new_type, %{context | vars: %{vars | version => data}}}
_ ->
{old_type, context}
end
_ ->
case gradual?(old_type) and compatible_intersection(old_type, type) do
{:ok, new_type} when new_type != old_type ->
data = %{
data
| type: new_type,
off_traces: new_trace(expr, new_type, stack, off_traces)
}
{new_type, %{context | vars: %{vars | version => data}}}
_ ->
{old_type, context}
end
end
end
@@ -141,7 +171,7 @@ defmodule Module.Types.Of do
{:ok, error_type(), context}
%{^version => %{type: old_type, off_traces: off_traces} = data} = vars ->
new_type = intersection(type, old_type)
new_type = opt_intersection(type, old_type)
data = %{
data
@@ -166,6 +196,24 @@ 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.
"""
@@ -187,14 +235,17 @@ defmodule Module.Types.Of do
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 Enum.all?(vars_conds, fn {_vars, cond} -> is_map_key(cond, version) end) do
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)
opt_union(acc, type)
end)
{_, context} = refine_body_var(version, type, expr, stack, context)
@@ -213,8 +264,8 @@ defmodule Module.Types.Of do
{Float, float()},
{Function, fun()},
{Integer, integer()},
{List, union(empty_list(), non_empty_list(term(), term()))},
{Map, open_map(__struct__: if_set(negation(atom())))},
{List, opt_union(empty_list(), non_empty_list(term(), term()))},
{Map, open_map(__struct__: {Module.Types.Descr.opt_negation(atom()), true})},
{Port, port()},
{PID, pid()},
{Reference, reference()},
@@ -243,7 +294,7 @@ defmodule Module.Types.Of do
if info = mode == :closed && Code.ensure_loaded?(struct) && struct.__info__(:struct) do
struct_type(struct, info)
else
open_map(__struct__: atom([struct]))
open_map(__struct__: {atom([struct]), false})
end
end
@@ -314,7 +365,7 @@ defmodule Module.Types.Of 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, [{key, {value_type, false}} | single], multiple}
_ ->
{dynamic?, domain, single, [{pos, value_type} | multiple]}
@@ -330,37 +381,47 @@ defmodule Module.Types.Of do
closed_map(non_multiple)
[{keys, type} | tail] ->
for key <- keys, t <- cartesian_map(tail) do
closed_map(non_multiple ++ [{key, type} | t])
products = cartesian_map(tail)
for key <- keys, product <- products do
closed_map(non_multiple ++ [{key, {type, false}} | product])
end
|> Enum.reduce(&union/2)
|> Enum.reduce(&opt_union/2)
end
{if(dynamic?, do: dynamic(map), else: map), context}
end
defp union_negated([], new_type, single, multiple) do
single = Enum.map(single, fn {key, old_type} -> {key, union(old_type, new_type)} end)
multiple = Enum.map(multiple, fn {keys, old_type} -> {keys, union(old_type, new_type)} end)
single =
Enum.map(single, fn
{key, {old_type, optional?}} ->
{key, {opt_union(old_type, new_type), optional?}}
end)
multiple =
Enum.map(multiple, fn {keys, old_type} -> {keys, opt_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
Enum.map_reduce(single, [], fn
{key, {old_type, optional?}}, matched ->
if key in negated do
{{key, {old_type, optional?}}, [key | matched]}
else
{{key, {opt_union(old_type, new_type), optional?}}, matched}
end
end)
multiple =
Enum.map(multiple, fn {keys, old_type} ->
{keys, union(old_type, new_type)}
{keys, opt_union(old_type, new_type)}
end)
{Enum.map(negated -- matched, fn key -> {key, not_set()} end) ++ single, multiple}
{Enum.map(negated -- matched, fn key -> {key, {none(), true}} end) ++ single, multiple}
end
defp pairs(pairs, expected, stack, context, of_fun) do
@@ -369,7 +430,7 @@ defmodule Module.Types.Of do
expected_value_type =
with {[key], [], []} <- pos_neg_domain,
{_, expected_value_type} <- map_fetch_key(expected, key) do
{_optional?, expected_value_type} <- map_fetch_key(expected, key) do
expected_value_type
else
_ -> term()
@@ -404,54 +465,66 @@ defmodule Module.Types.Of do
[[]]
[{keys, type} | tail] ->
for key <- keys, t <- cartesian_map(tail), do: [{key, type} | t]
products = cartesian_map(tail)
for key <- keys, product <- products, do: [{key, {type, false}} | product]
end
end
@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)
when is_atom(struct) do
{_info, context} = struct_info(struct, meta, stack, context)
{info, context} = struct_info(struct, :expr, meta, stack, context, true)
# 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
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
{_optional?, expected_value_type} -> expected_value_type
_ -> 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, false}}, context}
end)
{closed_map([{:__struct__, atom([struct])} | args_types]), context}
{closed_map([__struct__: {atom([struct]), false}] ++ args_types), context}
end
end
@doc """
Returns `__info__(:struct)` information about a struct.
"""
def struct_info(struct, meta, stack, context) do
def struct_info(struct, kind, meta, stack, context, must_exist? \\ false) do
case stack.no_warn_undefined do
%Macro.Env{} = env ->
case :elixir_map.maybe_load_struct_info(meta, struct, [], false, env) do
case :elixir_map.maybe_load_struct_info(meta, struct, :soft, env) do
{:ok, info} -> {info, context}
{:error, desc} -> raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
{:error, _desc} -> {nil, context}
end
_ ->
# Fetch the signature to validate for warnings.
{_, context} = Module.Types.Apply.signature(struct, :__struct__, 0, meta, stack, context)
info =
struct.__info__(:struct) ||
raise "expected #{inspect(struct)} to return struct metadata, but got none"
Code.ensure_loaded?(struct) and function_exported?(struct, :__info__, 1) and
struct.__info__(:struct)
{info, context}
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
end
end
@@ -462,12 +535,27 @@ defmodule Module.Types.Of do
# we introduce typed structs. They are only used by exceptions.
def struct_type(struct, info, args_types \\ []) do
term = dynamic()
pairs = for %{field: field} <- info, do: {field, term}
pairs = [{:__struct__, atom([struct])} | pairs]
pairs = if args_types == [], do: pairs, else: pairs ++ args_types
pairs = for %{field: field} <- info, do: {field, {term, false}}
pairs = [{:__struct__, {atom([struct]), false}} | pairs]
pairs =
if args_types == [] do
pairs
else
pairs ++ args_types
end
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
@doc """
@@ -476,47 +564,58 @@ defmodule Module.Types.Of do
In the stack, we add nodes such as <<expr>>, <<..., expr>>, etc,
based on the position of the expression within the binary.
"""
def bitstring(meta, parts, kind, stack, context) do
context = bitstring(parts, kind, stack, context)
case Keyword.get(meta, :alignment, :unknown) do
:unknown -> {bitstring(), context}
0 -> {binary(), context}
_ -> {bitstring_no_binary(), context}
end
def bitstring([], _kind, _stack, context) do
{binary(), context}
end
defp bitstring([], _kind, _stack, context) do
context
def bitstring([head], kind, stack, context) do
{alignment, context} = bitstring_segment(head, kind, [head], stack, context)
{alignment_to_type(alignment), context}
end
defp bitstring([head], kind, stack, context) do
bitstring_segment(head, kind, [head], stack, context)
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)
end
defp bitstring([head | tail], kind, stack, context) do
context = bitstring_segment(head, kind, [head, @suffix], stack, context)
bitstring_tail(tail, kind, stack, context)
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}
end
defp bitstring_tail([last], kind, stack, context) do
bitstring_segment(last, kind, [@prefix, last], stack, context)
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)
end
defp bitstring_tail([head | tail], kind, stack, context) do
context = bitstring_segment(head, kind, [@prefix, head, @suffix], stack, context)
bitstring_tail(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)
when is_binary(left) or is_number(left) do
specifier_size(kind, right, stack, context)
{_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
end
defp bitstring_segment({:"::", meta, [left, right]}, kind, args, stack, context) do
type = specifier_type(kind, right)
{type, alignment_type} = specifier_type(kind, right)
expr = {:<<>>, meta, args}
{actual, context} =
@@ -525,7 +624,7 @@ defmodule Module.Types.Of do
Module.Types.Pattern.of_match_var(left, type, expr, stack, context)
:guard ->
Module.Types.Pattern.of_guard(left, {false, type}, expr, stack, context)
Module.Types.Pattern.of_guard(left, type, expr, stack, context)
:expr ->
left = annotate_interpolation(left, right)
@@ -533,10 +632,26 @@ defmodule Module.Types.Of do
end
if compatible?(actual, type) do
specifier_size(kind, right, stack, context)
{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
else
error = {:badbinary, kind, meta, expr, type, actual, context}
error(error, meta, stack, context)
{:unknown, error(error, meta, stack, context)}
end
end
@@ -552,39 +667,48 @@ defmodule Module.Types.Of do
end
defp specifier_type(kind, {:-, _, [left, _right]}), do: specifier_type(kind, left)
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: @bitstring
defp specifier_type(_kind, {:bitstring, _, _}), do: @bitstring
defp specifier_type(_kind, {:bytes, _, _}), do: @binary
defp specifier_type(_kind, {:binary, _, _}), do: @binary
defp specifier_type(_kind, _specifier), do: @integer
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_size(kind, {:-, _, [left, right]}, stack, context) do
specifier_size(kind, right, stack, specifier_size(kind, left, stack, context))
defp specifier_size(kind, {:-, _, [left, right]}, stack, align_context) do
specifier_size(kind, right, stack, specifier_size(kind, left, stack, align_context))
end
defp specifier_size(:expr, {:size, _, [arg]} = expr, stack, context)
when not is_integer(arg) do
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
{actual, context} = Module.Types.Expr.of_expr(arg, integer(), expr, stack, context)
compatible_size(actual, expr, stack, context)
{:unknown, compatible_size(actual, expr, stack, context)}
end
defp specifier_size(match_or_guard, {:size, _, [arg]} = expr, stack, context)
when not is_integer(arg) do
{actual, context} = Module.Types.Pattern.of_size(match_or_guard, arg, expr, stack, context)
compatible_size(actual, expr, stack, context)
defp specifier_size(_match_or_guard, {:size, _, [arg]} = expr, stack, {_, context}) do
{actual, context} = Module.Types.Pattern.of_guard(arg, integer(), expr, stack, context)
{:unknown, compatible_size(actual, expr, stack, context)}
end
defp specifier_size(_kind, _specifier, _stack, context) do
context
# 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
end
defp compatible_size(actual, expr, stack, context) do
@@ -742,6 +866,37 @@ 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
+58 -7
View File
@@ -35,8 +35,19 @@ defmodule Module.Types.Traverse do
end)
end
# Structs, map update, tail operator
def of_expr({op, _meta, [left, right]}, stack, context) when op in [:%, :|] do
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
@@ -46,9 +57,9 @@ defmodule Module.Types.Traverse 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
# Skip the left side (pattern), only traverse right
# 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
@@ -65,8 +76,9 @@ defmodule Module.Types.Traverse do
end)
end
# Treat -> as patterns for simplicity
def of_expr({:->, _, [_head, body]}, stack, context) do
# 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
@@ -150,6 +162,45 @@ defmodule Module.Types.Traverse 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
+2 -4
View File
@@ -39,7 +39,7 @@ defmodule Node do
Currently supported options are:
* `:name_domain` - determines the host name part of the node name. If `:longnames`,
fully qualified domain names will be used which also is the default.
fully qualified domain names will be used, which is also the default.
If `:shortnames`, only the short name of the host will be used.
* `:net_ticktime` - The tick time to use in seconds. Defaults to the value of the
@@ -47,7 +47,7 @@ defmodule Node do
See [the `kernel` documentation](https://www.erlang.org/doc/apps/kernel/kernel_app.html)
for more information.
* `net_tickintensity` - The tick intensity to use. Defaults to the value of the
* `:net_tickintensity` - The tick intensity to use. Defaults to the value of the
`net_tickintensity` configuration under Erlang's `kernel` application.
See [the `kernel` documentation](https://www.erlang.org/doc/apps/kernel/kernel_app.html)
for more information.
@@ -338,8 +338,6 @@ defmodule Node do
The default node is `Node.self/0`, the local node. If `node` is the local node,
the function also sets the cookie of all other unknown nodes to `cookie`.
This function will raise `FunctionClauseError` if the given `node` is not alive.
"""
@spec set_cookie(t, atom) :: true
def set_cookie(node \\ Node.self(), cookie) when is_atom(cookie) do
+12 -6
View File
@@ -43,7 +43,7 @@ defmodule OptionParser do
defmodule ParseError do
@moduledoc """
An exception raised when parsing option fails.
An exception raised when parsing an option fails.
For example, see `OptionParser.parse!/2`.
"""
@@ -387,7 +387,7 @@ defmodule OptionParser do
do_parse(rest, config, new_opts, args, invalid, all?)
{:invalid, option, value, rest} ->
# the option exist but it has wrong value
# the option exists but it has wrong value
do_parse(rest, config, opts, args, [{option, value} | invalid], all?)
{:undefined, option, _value, rest} ->
@@ -463,7 +463,7 @@ defmodule OptionParser do
defp next_with_config(["--" <> option | rest], config) do
{option, value} = split_option(option)
if String.contains?(option, ["_"]) do
if String.contains?(option, "_") do
{:undefined, "--" <> option, value, rest}
else
tagged = tag_option(option, config)
@@ -852,7 +852,7 @@ defmodule OptionParser do
|> to_existing_key(allow_nonexistent_atoms?)
end
defp to_existing_key(option, true), do: String.to_atom(option)
defp to_existing_key(option, true), do: String.to_unsafe_atom(option)
defp to_existing_key(option, false) do
try do
@@ -952,8 +952,14 @@ defmodule OptionParser do
case types |> List.delete(:keep) |> List.first(:string) do
:boolean ->
base = "#{to_switch(name)}, #{to_switch(name, "--no-")}"
add_aliases(base, name, reverse_aliases)
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
type ->
base = "#{to_switch(name)} #{String.upcase(Atom.to_string(type))}"
+8 -6
View File
@@ -170,6 +170,8 @@ 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 =
@@ -366,7 +368,7 @@ defmodule PartitionSupervisor do
"""
@doc since: "1.18.0"
@spec resize!(name(), non_neg_integer()) :: non_neg_integer()
def resize!(name, partitions) when is_integer(partitions) do
def resize!(name, partitions) when is_name(name) and is_integer(partitions) do
supervisor =
GenServer.whereis(name) || exit({:noproc, {__MODULE__, :resize!, [name, partitions]}})
@@ -421,8 +423,8 @@ defmodule PartitionSupervisor do
Returns the number of partitions for the partition supervisor.
"""
@doc since: "1.14.0"
@spec partitions(name()) :: pos_integer()
def partitions(name) do
@spec partitions(name()) :: non_neg_integer()
def partitions(name) when is_name(name) do
name |> table() |> partitions(name)
end
@@ -470,7 +472,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_atom(name) or elem(name, 0) == :via do
def which_children(name) when is_name(name) do
Supervisor.which_children(name)
end
@@ -498,7 +500,7 @@ defmodule PartitionSupervisor do
supervisors: non_neg_integer,
workers: non_neg_integer
}
def count_children(supervisor) when is_atom(supervisor) do
def count_children(supervisor) when is_name(supervisor) do
Supervisor.count_children(supervisor)
end
@@ -514,7 +516,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_atom(supervisor) do
def stop(supervisor, reason \\ :normal, timeout \\ :infinity) when is_name(supervisor) do
Supervisor.stop(supervisor, reason, timeout)
end
+27 -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)
_ -> path
_ -> IO.chardata_to_string(path)
end
end
@@ -699,6 +699,32 @@ defmodule Path do
end
end
@doc """
Safely joins two paths.
Returns `{:ok, path}` if `right` is safe to append to `left`, or `:error`
otherwise. See `safe_relative/2` for the exact safety rules applied to `right`.
## Examples
iex> Path.safe_join("foo", "bar")
{:ok, "foo/bar"}
iex> Path.safe_join("foo", "../bar")
:error
iex> Path.safe_join("foo", "/bar")
:error
"""
@doc since: "1.21.0"
@spec safe_join(t, t) :: {:ok, t} | :error
def safe_join(left, right) do
with {:ok, right} <- safe_relative(right, left) do
{:ok, join(left, right)}
end
end
@doc ~S"""
Splits the path into a list at the path separator.
+6 -6
View File
@@ -61,7 +61,7 @@ defmodule Port do
* `{pid, {:connect, new_pid}}` - sets the `new_pid` as the new owner of
the port. Once a port is opened, the port is linked and connected to the
caller process and communication to the port only happens through the
connected process. This message makes `new_pid` the new connected processes.
connected process. This message makes `new_pid` the new connected process.
Unless the port is dead, the port will reply to the old owner with
`{port, :connected}`. See `connect/2`.
@@ -88,10 +88,10 @@ defmodule Port do
> On Unix systems, arguments are passed to a new operating system
> process as an array of strings but on Windows it is up to the child
> process to parse them and some Windows programs may apply their own
> rules, which are inconsistent with the standard C runtime `argv` parsing
> rules, which are inconsistent with the standard C runtime `argv` parsing.
>
> This is particularly troublesome when invoking `.bat` or `.com` files
> as these run implicitly through `cmd.exe`, whose argument parsing is
> This is particularly troublesome when invoking `.bat`, `.cmd`, or `.com`
> files as these run implicitly through `cmd.exe`, whose argument parsing is
> vulnerable to malicious input and can be used to run arbitrary shell
> commands.
>
@@ -99,7 +99,7 @@ defmodule Port do
> files or `.com` applications, you must not pass untrusted input as
> arguments to the program. You may avoid accidentally executing them
> by explicitly passing the extension of the program you want to run,
> such as `.exe`, and double check the program is indeed not a batch
> such as `.exe`, and double-checking the program is indeed not a batch
> file or `.com` application.
>
> This affects both `spawn` and `spawn_executable`.
@@ -156,7 +156,7 @@ defmodule Port do
We do not always have control over how third-party software terminates.
If necessary, one workaround is to wrap the child application in a script that
checks whether stdin has been closed. Here is such a script that has been
checks whether stdin has been closed. Here is such a script that has been
verified to work on bash shells:
#!/usr/bin/env bash
+38 -16
View File
@@ -79,10 +79,11 @@ defmodule Process do
@typedoc """
A process destination.
A remote or local PID, a local port, a locally registered name, or a tuple in
the form of `{registered_name, node}` for a registered name at another node.
A remote or local PID, a local port, a process alias, a locally registered name,
or a tuple in the form of `{registered_name, node}` for a registered name at
another node.
"""
@type dest :: pid | port | (registered_name :: atom) | {registered_name :: atom, node}
@type dest :: pid | port | alias | (registered_name :: atom) | {registered_name :: atom, node}
@doc """
Tells whether the given process is alive on the local node.
@@ -226,7 +227,7 @@ defmodule Process do
>
> The functions `Kernel.exit/1` and `Process.exit/2` are
> named similarly but provide very different functionalities. The
> `Kernel:exit/1` function should be used when the intent is to stop the current
> `Kernel.exit/1` function should be used when the intent is to stop the current
> process while `Process.exit/2` should be used when the intent is to send an
> exit signal to another process. Note also that `Kernel.exit/1` can be caught
> with `try/1` while `Process.exit/2` can only be handled by trapping exits and
@@ -339,7 +340,7 @@ defmodule Process do
@doc """
Sends a message to the given `dest`.
`dest` may be a remote or local PID, a local port, a locally
`dest` may be a remote or local PID, a local port, a process alias, a locally
registered name, or a tuple in the form of `{registered_name, node}` for a
registered name at another node.
@@ -387,8 +388,6 @@ 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
@@ -832,12 +831,16 @@ defmodule Process do
Inlined by the compiler.
"""
@spec flag(:async_dist, boolean) :: boolean
@spec flag(:error_handler, module) :: module
@spec flag(:fullsweep_after, non_neg_integer) :: non_neg_integer
@spec flag(:max_heap_size, heap_size) :: heap_size
# :off_heap | :on_heap twice because :erlang.message_queue_data() is not exported
@spec flag(:message_queue_data, :off_heap | :on_heap) :: :off_heap | :on_heap
@spec flag(:min_bin_vheap_size, non_neg_integer) :: non_neg_integer
@spec flag(:min_heap_size, non_neg_integer) :: non_neg_integer
@spec flag(:monitor_nodes, term) :: term
@spec flag({:monitor_nodes, term}, term) :: term
@spec flag(:priority, priority_level) :: priority_level
@spec flag(:save_calls, 0..10_000) :: 0..10_000
@spec flag(:sensitive, boolean) :: boolean
@@ -980,10 +983,34 @@ defmodule Process do
defdelegate unalias(alias), to: :erlang
@doc """
Add a descriptive term to the current process.
Returns the label set for the process `pid` as set with `set_label/1`
or `:proc_lib.set_label/1`.
The term does not need to be unique, and in Erlang/OTP 27+ will be shown in
Observer and in crash logs.
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 """
Adds 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.
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}`.
@@ -997,12 +1024,7 @@ defmodule Process do
"""
@doc since: "1.17.0"
@spec set_label(term()) :: :ok
def set_label(label) do
# TODO: switch to `:proc_lib.set_label/2` when we require Erlang/OTP 27+
Process.put(:"$process_label", label)
# mimic return value of `:proc_lib.set_label/2`
:ok
end
defdelegate set_label(label), to: :proc_lib
@compile {:inline, nilify: 1}
defp nilify(:undefined), do: nil

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