Compare commits

...
117 Commits
Author SHA1 Message Date
José Valim 42fb7d2be3 Back to take 1 2026-06-25 21:31:19 +02:00
José Valim 332f2ca791 Take 2 2026-06-25 21:00:54 +02:00
José Valim 082e08664e Take 1 2026-06-25 20:35:34 +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
198 changed files with 6224 additions and 3539 deletions
+2 -2
View File
@@ -42,7 +42,7 @@ jobs:
env:
ERLC_OPTS: "warnings_as_errors"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
@@ -123,7 +123,7 @@ jobs:
- name: Configure Git
run: git config --global core.autocrlf input
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
+5 -5
View File
@@ -28,16 +28,16 @@ jobs:
build-mode: none
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
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@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
category: "/language:${{matrix.language}}"
@@ -48,8 +48,8 @@ jobs:
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Run zizmor
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
- name: Checkout project
id: checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
fail-fast: false
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
fail-fast: false
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
+4 -4
View File
@@ -43,7 +43,7 @@ jobs:
--draft \
"$GITHUB_REF_NAME"
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
# zizmor: ignore[artipacked]
if: github.ref_type == 'branch'
@@ -82,7 +82,7 @@ jobs:
otp_version: "29.0"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
@@ -152,7 +152,7 @@ jobs:
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: "Sign files with Trusted Signing"
uses: azure/trusted-signing-action@b443cf8ea4124818d2ea9f043cba29fc3ec47b16 # v1.2.0
uses: azure/trusted-signing-action@c7ab2a863ab5f9a846ddb8265964877ef296ee82 # v2.0.0
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
with:
endpoint: https://eus.codesigning.azure.net/
@@ -205,7 +205,7 @@ jobs:
- name: Checkout project
id: checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
name: Notify
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
Symlink
+1
View File
@@ -0,0 +1 @@
CONTRIBUTING.md
+29 -398
View File
@@ -4,433 +4,64 @@
SPDX-FileCopyrightText: 2012 Plataformatec
-->
# Changelog for Elixir v1.20
# Changelog for Elixir v1.21
This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
## Type system improvements
Elixir's type system now understands all language constructs and can infer types for your function definitions, using typing information from Elixir's standard library and your dependencies, to find verified bugs and dead code.
This has been achieved through a series of improvements, such as type refinement across clauses, occurrence typing, typing of map keys and domains, and more.
### 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).
### Whole-body type inference
Elixir also performs inference based on the function body itself. Take the following code:
```elixir
def add_foo_and_bar(data) do
data.foo + data.bar
end
```
Elixir now infers that the function expects a `map` as first argument, and the map must have the keys `.foo` and `.bar` whose values are either `integer()` or `float()`. The return type will be either `integer()` or `float()`.
Here is another example:
```elixir
def sum_to_string(a, b) do
Integer.to_string(a + b)
end
```
Even though the `+` operator works with both integers and floats, Elixir infers that `a` and `b` must be both integers, as the result of `+` is given to a function that expects an integer. The inferred type information is then used during type checking to find possible typing errors. The typing inferred from your dependencies are also used to help infer more precise types for your own applications.
### Typing across clauses
Elixir now infers the type of a given clause based on previous clauses. Let's see an example:
```elixir
case System.get_env("SOME_VAR") do
nil -> :not_found
value -> {:ok, String.upcase(value)}
end
```
`System.get_env("SOME_VAR")` returns either `nil` or a `binary()`. Because the first clause matches on `nil`, the type system knows `value` can no longer be `nil`, and therefore it must only be a `binary()`, which allows the second clause to also type check without violations.
This type inference across clauses also helps the type system find redundant clauses and dead code in existing codebases. Elixir v1.20 also implements occurrence typing for `cond`, `case`, and `with`, providing more precise types within each clause.
### Typing of atom and domain keys in maps
Maps were one of the first data-structures we implemented within the Elixir type system however, up to this point, they only supported atom keys. If they had additional keys, those keys were simply marked as `dynamic()`.
As of Elixir v1.20, we can track all possible domains as map keys. For example, the map:
```elixir
%{123 => "hello", 456.0 => :ok}
```
will have the type:
```elixir
%{integer() => binary(), float() => :ok}
```
It is also possible to mix domain keys, as above, with atom keys, yielding the following:
```elixir
%{integer() => integer(), root: integer()}
```
This system is an implementation of [Typing Records, Maps, and Structs, by Giuseppe Castagna (2023)](https://www.irif.fr/~gc/papers/icfp23.pdf).
### Typing of map operations
We have typed the majority of the functions in the `Map` module, allowing the type system to track how keys are added, updated, and removed across all possible key types.
For example, imagine we are calling the following `Map` functions with a variable `map`, which we don't know the exact shape of, and an atom key:
```elixir
Map.put(map, :key, 123)
#=> returns type %{..., key: integer()}
Map.delete(map, :key)
#=> returns type %{..., key: not_set()}
```
As you can see, we track when keys are set and also when they are removed.
Some operations, like `Map.replace/3`, only replace the key if it exists, and that is also propagated by the type system:
```elixir
Map.replace(map, :key, 123)
#=> returns type %{..., key: if_set(integer())}
```
In other words, if the key exists, it would have been replaced by an integer value. Furthermore, whenever calling a function in the `Map` module and the given key is statically proven to never exist in the map, an error is emitted.
By combining full type inference with bang operations like `Map.fetch!/2`, `Map.pop!/2`, `Map.replace!/3`, and `Map.update!/3`, Elixir is able to propagate information about the desired keys. Take this module:
```elixir
defmodule User do
def name(map), do: Map.fetch!(map, :name)
end
defmodule CallsUser do
def calls_name do
User.name(%{})
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()})
type warning 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/).
## Compile-time improvements
Elixir's v1.20 improves compilation times once more, especially on applications with many cores.
It also introduces a new compiler option called `:module_definition`, which if the module definition should be `:compiled` (the default) or `:interpreted`. Note this does not affect the `.beam` file written to disk, only how the contents inside `defmodule` are executed. Using the `:interpreted` mode may offer better compilation times for large projects, especially on machines with high core count, however, it comes with some downsides:
* 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
You can enable it by setting `elixirc_options: [module_definition: :interpreted]` in your `mix.exs`.
## v1.20.0-rc.6
This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
### 1. Enhancements
#### Elixir
* [Kernel] Perform type inference across applications
### 2. Bug fixes
#### Elixir
* [Kernel] Fix type checker bug when validating a `case` inside a `cond` condition (regression)
* [Kernel] Preserve evaluation order when rewriting function calls from Elixir modules into Erlang ones
#### Mix
* [mix test] Respect --raise when mix test --warnings-as-errors passes with warnings
### 3. Hard deprecations
#### Mix
* [mix compile.elixir] `xref: [exclude: ...]` in your `mix.exs` is deprecated in favor of `elixirc_options: [no_warn_undefined: ...]`
## v1.20.0-rc.5 (2026-05-13)
## v1.21.0-dev
### 1. Enhancements
#### EEx
* [EEx] Optimize compiler by flattening expr list only once
* [EEx] Support splitting middle expressions across EEx clauses
#### Elixir
* [Base] Optimize Base validation functions by using SWAR techniques
* [Float] Optimize `Float.round/2` by avoiding big integers
* [Inspect] Increase inspect limit to help print deeply nested data structures
* [Inspect] Support printing Erlang records (using Erlang notation)
* [Kernel] Add occurrence typing on `case`, `cond`, and `with`
* [Registry] Switch `{:duplicate, :key}` key_ets to ordered_set with composite keys
* [String] SWAR-optimize ASCII fast paths in `String.length/1` and `String.slice/3`
* [Access] Add support for keyword lists in `Access.key/2` and `Access.key!/1`
* [Code] Add support for the `:erlc_options` compiler option
* [Kernel] Warn on binary patterns with segments that are not byte-aligned
* [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
* [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] Show remaining runs when using `--repeat-until-failure`
#### IEx
* [IEx.Helpers] Add `source/1`
#### Mix
* [mix app.tree] Support `--output` option
* [mix deps.tree] Support `--output` option
* [mix help] Support printing docs for types and callbacks
* [mix format] Support `--no-compile` option
* [mix source] Add `mix source MODULE` to print or open a given module/function location
### 2. Potential breaking changes
#### Elixir
* [Kernel] Disallow raw CR line ending in strings, comments and after `?` for security reasons
### 3. Bug fixes
#### Elixir
* [Kernel] Fix a compiler crash when importing a module with `only: :sigils` option when the imported module exports non-sigil symbols with `sigil_` prefix
* [Kernel] Reject negative Duration in `to_timeout/1`
* [Macro] Fix generation of heredocs in `Macro.to_string/1` with escaped trailing newline
* [Path] Consistently return path as binary in `Path.relative_to_cwd/2`
* [Stream] Raise in `Stream.cycle/1` when enumerable reduce call yields no elements
* [String] Support empty pattern list in `String.count/2`
#### Logger
* [Logger] Persist log level to app env in `Logger.configure/1`
#### Mix
* [Mix] Use `non_executable_binary_to_term` on loopback pubsub
* [mix compile.elixir] Fix scenario where Elixir would tag mtimes in the future
## v1.20.0-rc.4 (2026-03-31)
This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
### 1. Enhancements
#### Elixir
* [Code] Add `:dbg_callback` option to eval functions
* [Code.Fragment] Allow preserving sigil metadata in `container_cursor_to_quoted`
* [File] Add support for `[:raw]` opts in `File.read/2`
* [Kernel] Show undefined function errors even when missing variables (this helps debug errors caused when the developer forgets to require a macro)
* [Module] Purge and delete modules if `after_compile/2` callback fails
* [PartitionSupervisor] Support via tuples in `count_children/1` and `stop/3`
* [Process] Add `Process.get_label/1`
#### Mix
* [mix deps] Allow overriding specific dependencies in `:override`
* [ExUnit.Assertions] Add `trace/3` helper
### 2. Bug fixes
#### Elixir
* [Integer] Fix `Integer.extended_gcd/2` returning negative GCD for zero base cases
* [Integer] Raise when negative out-of-range digits are given to `Integer.undigits/2`
* [Kernel] Protocols should not add compile-time dependencies on `Any` implementation
* [Kernel] Ensure structs trigger recompilation for type checking purposes (regression)
* [Kernel] Ensure type information propagate across `hd/tl` in guards (regression)
* [Keyword] Raise `ArgumentError` in `Keyword.from_keys/2` for non-atom keys
* [URI] Fix `URI.merge` leaking `:+` marker when base path is empty string
* [Kernel] Fix expansion of rebound variables in bitstring size expressions
* [Kernel] Fix variables defined in one default argument leaking into subsequent default arguments
* [Kernel.Typespec] Preserve metadata when proxying to Elixir typespecs
#### ExUnit
* [ExUnit.Diff] Avoid false positives when diffing bitstrings
* [ExUnit.CaptureLog] Fix the typespec for capture log options
#### Mix
* [mix deps] Use config files to pass project state to avoid argv limits on Windows when using `MIX_OS_DEPS_COMPILE_PARTITION_COUNT`
* [mix compile] Fix compile env change triggering full recompilation of path dependencies
* [mix compile] Add a build lock around protocol consolidation in umbrellas
* [mix compile] Ensure compilation of sibling deps do not mark path deps as changed
* [mix test] Fix `--warnings-as-errors` not catching misnamed test file warnings
## v1.20.0-rc.3 (2026-03-09)
### 1. Enhancements
#### IEx
* [IEx] Optimize autocompleting modules
### 2. Bug fixes
#### Elixir
* [Enum] Fix `Enum.slice/2` for ranges with step > 1 sliced by step > 1
* [File] Allowing preserving directory permissions in `File.cp_r/3`
* [File] Fix `File.cp_r/3` infinite loop with symlink cycles
* [File] Fix `File.cp_r/3` infinite loop when copying into subdirectory of source
* [File] Warn when defining `@type record()`, fixes CI on Erlang/OTP 29
* [File] Fix `File.Stream` `Enumerable.count` for files without trailing newline
* [Float] Fix `Float.parse/1` inconsistent error handling for non-scientific notation overflow
* [Kernel] Process fields even when structs are unknown (regression)
* [Kernel] Improve performance on several corner cases in the type system (regression)
* [Kernel] Fix regression when using `Kernel.in/2` in defguard (regression)
## v1.20.0-rc.2 (2026-03-04)
### 1. Enhancements
#### Elixir
* [Code] Add `module_definition: :interpreted` option to `Code` which allows module definitions to be evaluated instead of compiled. In some applications/architectures, this can lead to drastic improvements to compilation times. Note this does not affect the generated `.beam` file, which will have the same performance/behaviour as before
* [Code] Make module purging opt-in and move temporary module deletion to the background to speed up compilation times
* [Integer] Add `Integer.popcount/1`
* [Kernel] Add type inference across clauses. For example, if one clause says `x when is_integer(x)`, then the next clause may no longer be an integer
* [Kernel] Detect and warn on redundant clauses
* [List] Add `List.first!/1` and `List.last!/1`
* Add Software Bill of Materials guide to the Documentation
#### Mix
* [mix compile] Add `module_definition: :interpreted` option to `Code` which allows module definitions to be evaluated instead of compiled. In some applications/architectures, this can lead to drastic improvements to compilation times. Note this does not affect the generated `.beam` file, which will have the same performance/behaviour as before
* [mix deps] Parallelize dep lock status checks during `deps.loadpaths`, improving boot times in projects with many git dependencies
### 2. Bug fixes
#### IEx
* [IEx] Ensure warnings emitted during IEx parsing are properly displayed/printed
* [IEx] Ensure pry works across remote nodes
#### Mix
* [mix compile.erlang] Topsort Erlang modules before compilation for proper dependency resolution
## v1.20.0-rc.1 (2026-01-13)
### 1. Bug fixes
#### Elixir
* [Kernel] Do not crash on map types with struct keys when performing type operations (regression)
* [Kernel] Mark the outcome of bitstring types as dynamic (regression)
* [Kernel] `<<expr::bitstring>>` will have type `binary` instead of `bitstring` if `expr` is a binary (regression)
* [Kernel] Do not crash on conditional variables when calling a function on a module which is represented by a variable (regression)
## v1.20.0-rc.0 (2026-01-09)
### 1. Enhancements
#### 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`
#### ExUnit
* [ExUnit.CaptureLog] Add `:formatter` option for custom log formatting
#### Mix
* [mix deps] Support filtering `mix deps` output
* [mix compile] Enforce `:elixirc_paths` to be a list of strings to avoid paths from being discarded (the only documented type was lists of strings)
* [mix test] Add `mix test --dry-run`
### 2. Potential breaking changes
#### Elixir
* `require SomeModule` no longer expands to the given module at compile-time, but it still returns the module at runtime. Note Elixir does not guarantee macros will expand to certain constructs, but since this can break code relying on the previous behaviour, such as `require(SomeMod).some_macro()`, we are adding this note to the CHANGELOG
* [Mix.SCM.Git] Raise if Git refspecs start with `-`
* [mix deps.compile] Preserve code paths and compiler options across OS partitions
### 3. Hard deprecations
#### Elixir
* [File] `File.stream!(path, modes, lines_or_bytes)` is deprecated in favor of `File.stream!(path, lines_or_bytes, modes)`
* [Kernel] Matching on the size inside a bit pattern now requires the pin operator for consistency, such as `<<x::size(^existing_var)>>`
* [Kernel.ParallelCompiler] `Kernel.ParallelCompiler.async/1` is deprecated in favor of `Kernel.ParallelCompiler.pmap/2`, which is more performant and addresses known limitations
* [Macro.Env] `Macro.Env.fetch_alias/2` and `Macro.Env.fetch_macro_alias/2` are deprecated, use `Macro.Env.expand_alias/4` instead
#### Logger
### 4. Soft deprecations
* [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`
#### Elixir
## v1.19
* [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`
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).
## 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).
+1 -1
View File
@@ -135,7 +135,7 @@ make docs
This will produce documentation sets for `elixir`, `eex`, `ex_unit`, `iex`, `logger`,
and `mix` under the `doc` directory. If you are planning to contribute documentation,
[please check our best practices for writing documentation](https://hexdocs.pm/elixir/writing-documentation.html).
[please check our best practices for writing documentation](https://elixir.hexdocs.pm/writing-documentation.html).
## Licensing and Compliance Requirements
+1 -1
View File
@@ -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
+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.5
1.21.0-dev
+1 -1
View File
@@ -6,7 +6,7 @@
set -e
ELIXIR_VERSION=1.20.0-rc.5
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.5
set ELIXIR_VERSION=1.21.0-dev
if ""%1""=="""" if ""%2""=="""" goto documentation
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
+40 -13
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,31 +198,31 @@ 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}
{: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
@@ -476,6 +497,12 @@ 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
+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}}
+32 -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
"""
+68 -14
View File
@@ -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
+9 -2
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.
@@ -529,6 +531,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
@@ -667,9 +670,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
+40 -36
View File
@@ -243,19 +243,19 @@ 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
@@ -273,19 +273,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 +302,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 +329,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
+17 -7
View File
@@ -314,7 +314,7 @@ 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 ++
@@ -1634,13 +1634,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
@@ -1753,6 +1759,10 @@ defmodule Code do
* `: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`.
+1 -1
View File
@@ -1457,7 +1457,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
]
+1
View File
@@ -204,6 +204,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}
+1 -1
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
+2 -2
View File
@@ -264,7 +264,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}
@@ -559,7 +559,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
+2 -2
View File
@@ -399,10 +399,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
+1 -1
View File
@@ -1442,7 +1442,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])
+3 -3
View File
@@ -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
+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`
+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
+1 -1
View File
@@ -332,7 +332,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
@@ -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:
+10 -12
View File
@@ -2109,7 +2109,7 @@ defmodule Kernel do
assert_no_match_or_guard_scope(__CALLER__.context, "!")
annotate_case(
[optimize_boolean: true, type_check: {:case, :!}],
[optimize_boolean: true, type_check: {:case, :"!!"}],
quote do
case unquote(value) do
x when unquote(x_is_false_or_nil()) -> false
@@ -4703,10 +4703,8 @@ 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 ->
case in_body? do
@@ -5250,12 +5248,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
@@ -6280,7 +6278,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
@@ -6899,7 +6897,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
@@ -7010,7 +7008,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
@@ -7019,7 +7017,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
@@ -7053,7 +7051,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
+10 -4
View File
@@ -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)
@@ -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)
+2 -2
View File
@@ -197,8 +197,8 @@ defmodule Kernel.ParallelCompiler do
deprecation warnings, and type checking should run. Defaults to `true`.
We recommend disabling it only for debugging purposes.
* `:profile` - if set to `:time` measure the compilation time of each compilation cycle
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`)
+14 -10
View File
@@ -860,29 +860,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
@@ -1007,6 +1007,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]
+13 -15
View File
@@ -329,21 +329,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), 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,9 +375,7 @@ defmodule Kernel.Utils do
end
# Prefaces `guard` with unquoted versions of `refs`.
defp unquote_refs_once(guard, refs, %{module: module} = env) do
env = %{env | context: nil}
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) ->
@@ -384,7 +388,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
@@ -393,12 +397,6 @@ defmodule Kernel.Utils do
{var, acc}
end
{{:., dot_meta, [:erlang, :orelse]}, meta, [left, right]}, acc ->
{Macro.expand({{:., dot_meta, [Kernel, :or]}, meta, [left, right]}, env), acc}
{{:., dot_meta, [:erlang, :andalso]}, meta, [left, right]}, acc ->
{Macro.expand({{:., dot_meta, [Kernel, :and]}, meta, [left, right]}, env), acc}
node, acc ->
{node, acc}
end)
+52 -30
View File
@@ -269,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, [], keyword, [])
validate_merge(keyword, values, [], keyword)
end
defp validate([{key, _} | keyword], values1, values2, original, bad_keys) when is_atom(key) do
case find_key!(key, values1, values2) do
{values1, values2} ->
validate(keyword, values1, values2, original, 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, original, bad_keys)
:error ->
validate(keyword, values1, values2, original, [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, original, []) do
{:ok, move_pairs!(values1, move_pairs!(values2, original))}
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, _original, 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, _original, []) 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}
@@ -1342,9 +1364,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
@@ -1369,9 +1391,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
@@ -1434,9 +1456,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
+54 -5
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]
@@ -1024,6 +1024,11 @@ defmodule List do
:lists.suffix(suffix, list)
end
@doc deprecated: "Use to_existing_atom/1 or to_unsafe_atom/1 instead"
def to_atom(charlist) do
:erlang.list_to_atom(charlist)
end
@doc """
Converts a charlist to an atom.
@@ -1034,21 +1039,24 @@ defmodule List do
## Examples
iex> List.to_atom(~c"Elixir")
iex> List.to_unsafe_atom(~c"Elixir")
:Elixir
iex> List.to_atom(~c"🌢 Elixir")
iex> List.to_unsafe_atom(~c"🌢 Elixir")
:"🌢 Elixir"
"""
@spec to_atom(charlist) :: atom
def to_atom(charlist) do
@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.
@@ -1079,6 +1087,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`.
+2 -2
View File
@@ -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 """
@@ -2699,7 +2699,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
+2 -2
View File
@@ -210,13 +210,13 @@ defmodule Macro.Env 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}")
# 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}")
+5 -9
View File
@@ -171,18 +171,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 """
+8 -7
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`.
@@ -1147,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
@@ -1158,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
+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)
+27 -6
View File
@@ -20,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()}
]
@@ -117,13 +118,13 @@ defmodule Module.ParallelChecker do
send(checker, {ref, :cached})
receive do
{^ref, :check} ->
{^ref, :check, profile} ->
# Set the compiler info so we can collect warnings
:erlang.put(:elixir_compiler_info, {pid, self()})
{warnings, errors} =
if module_tuple do
check_module(module_tuple, {checker, table}, log?)
check_module(module_tuple, {checker, table}, log?, profile)
else
{[], []}
end
@@ -262,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
@@ -276,9 +277,13 @@ defmodule Module.ParallelChecker do
definitions
)
type_warnings =
profile(module, profile, fn ->
Module.Types.warnings(module, file, attrs, definitions, no_warn_undefined, cache)
end)
{warnings, errors} =
module
|> Module.Types.warnings(file, attrs, definitions, no_warn_undefined, cache)
type_warnings
|> Kernel.++(behaviour_warnings)
|> group_diagnostics()
|> emit_diagnostics(file, log?)
@@ -577,6 +582,7 @@ defmodule Module.ParallelChecker do
schedulers: schedulers,
threshold: threshold,
callback: callback,
profile: Keyword.get(options, :profile),
protocols: [],
table: table
}
@@ -653,9 +659,24 @@ defmodule Module.ParallelChecker do
end
defp run_checkers(%{modules: [{module, pid, ref} | modules]} = state) do
send(pid, {ref, :check})
send(pid, {ref, :check, state.profile})
timer = :erlang.send_after(state.threshold, self(), {__MODULE__, :timeout, module, pid})
spawned = Map.put(state.spawned, module, timer)
run_checkers(%{state | modules: modules, spawned: spawned})
end
defp profile(module, :time, fun) do
{time, result} = :timer.tc(fun)
IO.puts(
:stderr,
"[profile] Type checked #{inspect(module)} in #{div(time, 1000)}ms"
)
result
end
defp profile(_module, _profile, fun) do
fun.()
end
end
+19 -16
View File
@@ -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
@@ -266,7 +266,7 @@ defmodule Module.Types do
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)
else
@@ -299,11 +299,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}))
@@ -316,9 +316,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)
@@ -326,9 +327,9 @@ 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}
case clauses do
@@ -341,6 +342,7 @@ defmodule Module.Types do
end
defp default_local_handler(meta, args, body, base_info, kind, fun, expected, stack, context) do
stack = with_file_meta(stack, meta)
guards = []
previous = Pattern.init_previous()
fresh_context = fresh_context(context)
@@ -385,7 +387,7 @@ defmodule Module.Types do
_ ->
inferred
|> Enum.map(fn {args, _} -> args end)
|> Enum.zip_with(fn types -> Enum.reduce(types, &Descr.union/2) 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)}
@@ -400,6 +402,7 @@ defmodule Module.Types do
Enum.reduce(clauses, {0, 0, Pattern.init_previous(), [], [], [], context}, fn
{meta, args, guards, body},
{index, total, previous, domain, mapping, inferred, acc_context} ->
stack = with_file_meta(stack, meta)
fresh_context = fresh_context(acc_context)
info = {base_info, args, guards}
@@ -462,7 +465,7 @@ defmodule Module.Types do
# the domain is computed by unioning their inferred types. However, their
# inferred types often have the different of the previous clauses:
#
# union(r3 ^ (c3 - c2 - c1), r2 ^ (c2 - c1), r1 ^ c1)
# opt_union(r3 ^ (c3 - c2 - c1), r2 ^ (c2 - c1), r1 ^ c1)
#
# Where `rN` represents the refinement in every function body.
#
@@ -493,9 +496,9 @@ defmodule Module.Types do
# Furthermore, the signature used in type checking is not refined in any way,
# so type checking is still sound.
if arg == head_arg do
Descr.union(Descr.upper_bound(no_prev_arg), d)
Descr.opt_union(Descr.upper_bound(no_prev_arg), d)
else
Descr.union(arg, d)
Descr.opt_union(arg, d)
end
| compute_domain(args_types, head_args_types, no_prev_args_types, domain)
]
@@ -506,7 +509,7 @@ defmodule Module.Types 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])
@@ -558,7 +561,7 @@ defmodule Module.Types do
# Allow exactly one differing argument. That one position is widened
# with union/2. A second difference means the clauses must stay separate.
defp union_args([existing_arg | existing], [arg | args], acc, false) do
union_args(existing, args, [Descr.union(existing_arg, arg) | acc], true)
union_args(existing, args, [Descr.opt_union(existing_arg, arg) | acc], true)
end
defp union_args([_ | _], [_ | _], _acc, true), do: nil
@@ -628,7 +631,7 @@ defmodule Module.Types do
mode: mode,
# The function for handling local calls
local_handler: handler,
# Reverse arrow handling (nil | :cache | :use)
# Reverse arrow handling (nil | :cache | :except_none | :include_none)
reverse_arrow: nil
}
end
+92 -64
View File
@@ -33,12 +33,14 @@ defmodule Module.Types.Apply do
|> Enum.map(fn {key, type} when is_atom(key) ->
tuple([atom([key]), type])
end)
|> Enum.reduce(&union/2)
|> Enum.reduce(&opt_union/2)
|> list()
end
fas = list(tuple([atom(), integer()]))
struct_info = list(closed_map(default: if_set(term()), field: atom()))
struct_info =
list(closed_map(default: if_set(term()), field: atom(), required: if_set(boolean())))
shared_info = [
attributes: list(tuple([atom(), list(term())])),
@@ -56,7 +58,7 @@ defmodule Module.Types.Apply do
exports_md5: binary(),
functions: fas,
macros: fas,
struct: struct_info |> union(atom([nil]))
struct: struct_info |> opt_union(atom([nil]))
] ++ shared_info
infos =
@@ -72,7 +74,7 @@ defmodule Module.Types.Apply do
module: atom(),
functions: fas,
consolidated?: boolean(),
impls: union(atom([:not_consolidated]), tuple([atom([:consolidated]), list(atom())]))}
impls: opt_union(atom([:not_consolidated]), tuple([atom([:consolidated]), list(atom())]))}
]
for {name, clauses} <- infos do
@@ -96,10 +98,10 @@ defmodule Module.Types.Apply do
send_destination =
pid()
|> union(reference())
|> union(port())
|> union(atom())
|> union(tuple([atom(), atom()]))
|> opt_union(reference())
|> opt_union(port())
|> opt_union(atom())
|> opt_union(tuple([atom(), atom()]))
basic_arith_2_args_clauses = [
{[integer(), integer()], integer()},
@@ -108,16 +110,16 @@ defmodule Module.Types.Apply do
{[float(), float()], float()}
]
args_or_arity = union(list(term()), integer())
args_or_none = union(list(term()), atom([:none]))
args_or_arity = opt_union(list(term()), integer())
args_or_none = opt_union(list(term()), atom([:none]))
extra_info = kw.(file: list(integer()), line: integer(), error_info: open_map())
raise_stacktrace =
list(
tuple([atom(), atom(), args_or_arity, extra_info])
|> union(tuple([atom(), atom(), args_or_arity]))
|> union(tuple([fun(), args_or_arity, extra_info]))
|> union(tuple([fun(), args_or_arity]))
|> opt_union(tuple([atom(), atom(), args_or_arity]))
|> opt_union(tuple([fun(), args_or_arity, extra_info]))
|> opt_union(tuple([fun(), args_or_arity]))
)
not_signature =
@@ -145,7 +147,8 @@ defmodule Module.Types.Apply do
{:erlang, :-, [{[integer()], integer()}, {[float()], float()}]},
{:erlang, :-, basic_arith_2_args_clauses},
{:erlang, :*, basic_arith_2_args_clauses},
{:erlang, :/, [{[union(integer(), float()), union(integer(), float())], float()}]},
{:erlang, :/,
[{[opt_union(integer(), float()), opt_union(integer(), float())], float()}]},
{:erlang, :"/=", [{[term(), term()], boolean()}]},
{:erlang, :"=/=", [{[term(), term()], boolean()}]},
{:erlang, :<, [{[term(), term()], boolean()}]},
@@ -175,12 +178,12 @@ defmodule Module.Types.Apply do
{:erlang, :bsr, [{[integer(), integer()], integer()}]},
{:erlang, :bxor, [{[integer(), integer()], integer()}]},
{:erlang, :byte_size, [{[bitstring()], integer()}]},
{:erlang, :ceil, [{[union(integer(), float())], integer()}]},
{:erlang, :ceil, [{[opt_union(integer(), float())], integer()}]},
{:erlang, :div, [{[integer(), integer()], integer()}]},
{:erlang, :error, [{[term()], none()}]},
{:erlang, :error, [{[term(), args_or_none], none()}]},
{:erlang, :error, [{[term(), args_or_none, kw.(error_info: open_map())], none()}]},
{:erlang, :floor, [{[union(integer(), float())], integer()}]},
{:erlang, :floor, [{[opt_union(integer(), float())], integer()}]},
{:erlang, :function_exported, [{[atom(), atom(), integer()], boolean()}]},
{:erlang, :integer_to_binary, [{[integer()], binary()}]},
{:erlang, :integer_to_binary, [{[integer(), integer()], binary()}]},
@@ -198,12 +201,12 @@ defmodule Module.Types.Apply do
{:erlang, :make_tuple, [{[integer(), term()], tuple()}]},
{:erlang, :map_size, [{[open_map()], integer()}]},
{:erlang, :node, [{[], atom()}]},
{:erlang, :node, [{[pid() |> union(reference()) |> union(port())], atom()}]},
{:erlang, :node, [{[pid() |> opt_union(reference()) |> opt_union(port())], atom()}]},
{:erlang, :not, not_signature},
{:erlang, :or, or_signature},
{:erlang, :raise, [{[atom([:error, :exit, :throw]), term(), raise_stacktrace], none()}]},
{:erlang, :rem, [{[integer(), integer()], integer()}]},
{:erlang, :round, [{[union(integer(), float())], integer()}]},
{:erlang, :round, [{[opt_union(integer(), float())], integer()}]},
{:erlang, :self, [{[], pid()}]},
{:erlang, :spawn, [{[fun(0)], pid()}]},
{:erlang, :spawn, [{mfargs, pid()}]},
@@ -213,7 +216,7 @@ defmodule Module.Types.Apply do
{:erlang, :spawn_monitor, [{mfargs, tuple([pid(), reference()])}]},
{:erlang, :split_binary, [{[binary(), integer()], tuple([binary(), binary()])}]},
{:erlang, :tuple_size, [{[open_tuple([])], integer()}]},
{:erlang, :trunc, [{[union(integer(), float())], integer()}]},
{:erlang, :trunc, [{[opt_union(integer(), float())], integer()}]},
# TODO: Replace term()/dynamic() by parametric types
{:erlang, :++,
@@ -242,12 +245,16 @@ defmodule Module.Types.Apply do
{Kernel, :put_elem, [{[open_tuple([]), integer(), term()], dynamic(open_tuple([]))}]},
## Lists
{:lists, :member, [{[term(), list(term())], boolean()}]},
{:lists, :member,
[
{[term(), empty_list()], atom([false])},
{[term(), non_empty_list(term())], boolean()}
]},
## Map
{Map, :delete, [{[open_map(), term()], open_map()}]},
{Map, :fetch,
[{[open_map(), term()], tuple([atom([:ok]), term()]) |> union(atom([:error]))}]},
[{[open_map(), term()], tuple([atom([:ok]), term()]) |> opt_union(atom([:error]))}]},
{Map, :fetch!, [{[open_map(), term()], term()}]},
{Map, :from_struct, [{[open_map()], open_map(__struct__: not_set())}]},
{Map, :get, [{[open_map(), term()], term()}]},
@@ -271,17 +278,19 @@ defmodule Module.Types.Apply do
{Tuple, :insert_at, [{[open_tuple([]), integer(), term()], dynamic(open_tuple([]))}]},
{:maps, :from_keys, [{[list(term()), term()], open_map()}]},
{:maps, :find,
[{[term(), open_map()], tuple([atom([:ok]), term()]) |> union(atom([:error]))}]},
[{[term(), open_map()], tuple([atom([:ok]), term()]) |> opt_union(atom([:error]))}]},
{:maps, :get, [{[term(), open_map()], term()}]},
{:maps, :is_key, [{[term(), open_map()], boolean()}]},
{:maps, :keys, [{[open_map()], list(term())}]},
{:maps, :put, [{[term(), term(), open_map()], open_map()}]},
{:maps, :remove, [{[term(), open_map()], open_map()}]},
{:maps, :take,
[{[term(), open_map()], tuple([term(), open_map()]) |> union(atom([:error]))}]},
[{[term(), open_map()], tuple([term(), open_map()]) |> opt_union(atom([:error]))}]},
{:maps, :to_list, [{[open_map()], list(tuple([term(), term()]))}]},
{:maps, :update, [{[term(), term(), open_map()], open_map()}]},
{:maps, :values, [{[open_map()], list(term())}]}
{:maps, :values, [{[open_map()], list(term())}]},
{String, :to_existing_atom, [{[binary(), non_empty_list(atom())], atom()}]},
{List, :to_existing_atom, [{[list(integer()), non_empty_list(atom())], atom()}]}
] do
[arity] = Enum.map(clauses, fn {args, _return} -> length(args) end) |> Enum.uniq()
@@ -294,7 +303,7 @@ defmodule Module.Types.Apply do
domain =
clauses
|> Enum.map(fn {args, _} -> args end)
|> Enum.zip_with(fn types -> Enum.reduce(types, &union/2) end)
|> Enum.zip_with(fn types -> Enum.reduce(types, &opt_union/2) end)
{:strong, domain, clauses}
end
@@ -311,9 +320,9 @@ defmodule Module.Types.Apply do
is_float: float(),
is_function: fun(),
is_integer: integer(),
is_list: union(empty_list(), non_empty_list(term(), term())),
is_list: opt_union(empty_list(), non_empty_list(term(), term())),
is_map: open_map(),
is_number: union(float(), integer()),
is_number: opt_union(float(), integer()),
is_pid: pid(),
is_port: port(),
is_reference: reference(),
@@ -325,7 +334,7 @@ defmodule Module.Types.Apply do
{:strong, [term()],
[
{[type], atom([true])},
{[negation(type)], atom([false])}
{[Module.Types.Descr.opt_negation(type)], atom([false])}
]}
def signature(:erlang, unquote(guard), 1),
@@ -339,7 +348,7 @@ defmodule Module.Types.Apply do
{:strong, [term(), integer(), integer()],
[
{[integer(), integer(), integer()], boolean()},
{[negation(integer()), integer(), integer()], atom([false])}
{[Module.Types.Descr.opt_negation(integer()), integer(), integer()], atom([false])}
]}
)
)
@@ -439,7 +448,7 @@ defmodule Module.Types.Apply do
# However, during inference, we type it as `a, b -> a and b` only.
{left_type, context} = of_fun.(left, expected, expr, stack, context)
{right_type, context} = of_fun.(right, expected, expr, stack, context)
result = union(left_type, right_type)
result = opt_union(left_type, right_type)
if error = mismatched_ordered_comparison(left_type, right_type, stack) do
remote_error(error, :erlang, name, 2, expr, stack, context)
@@ -664,10 +673,14 @@ defmodule Module.Types.Apply do
{type, context} = of_fun.(literal, term(), expr, stack, context)
if singleton?(type) do
acc = if polarity, do: union(acc, type), else: intersection(acc, negation(type))
acc =
if polarity,
do: opt_union(acc, type),
else: opt_intersection(acc, Module.Types.Descr.opt_negation(type))
{acc, all_singleton?, context}
else
acc = if polarity, do: union(acc, type), else: acc
acc = if polarity, do: opt_union(acc, type), else: acc
{acc, false, context}
end
end)
@@ -705,9 +718,9 @@ defmodule Module.Types.Apply do
@empty_list empty_list()
@non_empty_list non_empty_list(term())
@list union(empty_list(), non_empty_list(term()))
@list opt_union(empty_list(), non_empty_list(term()))
@empty_map empty_map()
@non_empty_map difference(open_map(), empty_map())
@non_empty_map opt_difference(open_map(), empty_map())
# Limit the size of tuples to 16 entries
# as otherwise we may create large nodes
@@ -767,7 +780,7 @@ defmodule Module.Types.Apply do
{tuple(List.duplicate(term(), literal)), true}
:tuple_size ->
{difference(open_tuple([]), tuple(List.duplicate(term(), literal))), true}
{opt_difference(open_tuple([]), tuple(List.duplicate(term(), literal))), true}
end
{actual, context} = of_fun.(arg, expected, expr, stack, context)
@@ -798,7 +811,7 @@ defmodule Module.Types.Apply do
# This logic mirrors the code in `Pattern.of_pattern_tree`
# If it is a singleton, we can always be precise
if singleton?(type) do
expected = if polarity, do: type, else: negation(type)
expected = if polarity, do: type, else: Module.Types.Descr.opt_negation(type)
{arg_type, context} = of_fun.(arg, expected, expr, stack, context)
result = if subtype?(arg_type, upper_bound(expected)), do: return, else: boolean()
@@ -906,13 +919,13 @@ defmodule Module.Types.Apply do
defp expected_order(_, :<, 0), do: :none
defp expected_order(:tuple_size, :<, size),
do: {difference(open_tuple([]), open_tuple(List.duplicate(term(), size))), true}
do: {opt_difference(open_tuple([]), open_tuple(List.duplicate(term(), size))), true}
defp expected_order(:tuple_size, :"=<", 0),
do: {tuple([]), true}
defp expected_order(:tuple_size, :"=<", size),
do: {difference(open_tuple([]), open_tuple(List.duplicate(term(), size + 1))), true}
do: {opt_difference(open_tuple([]), open_tuple(List.duplicate(term(), size + 1))), true}
defp expected_order(:tuple_size, :>, size),
do: {open_tuple(List.duplicate(term(), size + 1)), true}
@@ -968,7 +981,7 @@ defmodule Module.Types.Apply do
{:strong, [term(), integer()],
[
{[type, integer()], atom([true])},
{[negation(type), integer()], atom([false])}
{[Module.Types.Descr.opt_negation(type), integer()], atom([false])}
]}
{info, filter_domain(info, expected, 2), context}
@@ -1110,7 +1123,7 @@ defmodule Module.Types.Apply do
defp remote_apply(Map, :fetch, _info, [map, key] = args_types, stack) do
case map_get(map, key) do
{_, value} ->
result = tuple([atom([:ok]), value]) |> union(atom([:error]))
result = tuple([atom([:ok]), value]) |> opt_union(atom([:error]))
{:ok, return(result, args_types, stack)}
:badmap ->
@@ -1131,7 +1144,7 @@ defmodule Module.Types.Apply do
defp remote_apply(Map, :get, _info, [map, key] = args_types, stack) do
case map_get(map, key) do
{:ok, value} -> {:ok, return(union(value, @nil_atom), args_types, stack)}
{:ok, value} -> {:ok, return(opt_union(value, @nil_atom), args_types, stack)}
:badmap -> {:error, badremote(Map, :get, args_types)}
:error -> {:error, {:badkeydomain, map, key, @nil_atom}}
end
@@ -1139,7 +1152,7 @@ defmodule Module.Types.Apply do
defp remote_apply(Map, :get, _info, [map, key, default] = args_types, stack) do
case map_get(map, key) do
{:ok, value} -> {:ok, return(union(value, default), args_types, stack)}
{:ok, value} -> {:ok, return(opt_union(value, default), args_types, stack)}
:badmap -> {:error, badremote(Map, :get, args_types)}
:error -> {:error, {:badkeydomain, map, key, default}}
end
@@ -1149,7 +1162,7 @@ defmodule Module.Types.Apply do
case fun_apply(fun, []) do
{:ok, default} ->
case map_get(map, key) do
{:ok, value} -> {:ok, return(union(value, default), args_types, stack)}
{:ok, value} -> {:ok, return(opt_union(value, default), args_types, stack)}
:badmap -> {:error, badremote(Map, :get_lazy, args_types)}
:error -> {:error, {:badkeydomain, map, key, default}}
end
@@ -1179,7 +1192,7 @@ defmodule Module.Types.Apply do
case map_update(map, key, not_set(), true, false) do
{value, descr, _errors} ->
value = union(value, default)
value = opt_union(value, default)
{:ok, return(tuple([value, descr]), args_types, stack)}
:badmap ->
@@ -1195,7 +1208,7 @@ defmodule Module.Types.Apply do
{:ok, default} ->
case map_update(map, key, not_set(), true, false) do
{value, descr, _errors} ->
value = union(value, default)
value = opt_union(value, default)
{:ok, return(tuple([value, descr]), args_types, stack)}
:badmap ->
@@ -1269,7 +1282,7 @@ defmodule Module.Types.Apply do
default
else
case fun_apply(fun, [arg_type]) do
{:ok, res} -> if optional?, do: union(res, default), else: res
{:ok, res} -> if optional?, do: opt_union(res, default), else: res
reason -> throw({:badapply, reason, [arg_type]})
end
end
@@ -1293,7 +1306,7 @@ defmodule Module.Types.Apply do
defp remote_apply(:maps, :find, _info, [key, map] = args_types, stack) do
case map_get(map, key) do
{_, value} ->
result = tuple([atom([:ok]), value]) |> union(atom([:error]))
result = tuple([atom([:ok]), value]) |> opt_union(atom([:error]))
{:ok, return(result, args_types, stack)}
:badmap ->
@@ -1327,7 +1340,7 @@ defmodule Module.Types.Apply do
map_and_maybe_empty_map =
case empty_list? do
true -> map
false -> difference(map, empty_map())
false -> opt_difference(map, empty_map())
end
{:ok, return(map_and_maybe_empty_map, args_types, stack)}
@@ -1372,7 +1385,7 @@ defmodule Module.Types.Apply do
defp remote_apply(:maps, :take, _info, [key, map] = args_types, stack) do
case map_update(map, key, not_set(), true, false) do
{value, descr, _errors} ->
result = union(tuple([value, descr]), atom([:error]))
result = opt_union(tuple([value, descr]), atom([:error]))
{:ok, return(result, args_types, stack)}
:badmap ->
@@ -1403,7 +1416,22 @@ defmodule Module.Types.Apply do
defp remote_apply(:maps, :values, _info, [map], stack) do
case map_to_list(map, fn _key, value -> value end) do
{:ok, list_type} -> {:ok, return(list_type, [map], stack)}
:badmap -> {:error, badremote(:maps, :keys, [map])}
:badmap -> {:error, badremote(:maps, :values, [map])}
end
end
defp remote_apply(mod, :to_existing_atom, info, [_string, list] = args_types, stack)
when mod in [String, List] do
# TODO remove once we add parametric types, this will just be:
# binary(), non_empty_list(a) -> a when a: atom()
case remote_apply(info, args_types, stack) do
{:ok, _} ->
{false, refined_atom} = list_of(list)
{:ok, refined_atom}
other ->
other
end
end
@@ -1446,10 +1474,10 @@ defmodule Module.Types.Apply do
Enum.reduce(modules, {none(), false, context}, fn module, {type, fallback?, context} ->
case signature(module, fun, arity, meta, stack, context) do
{{:strong, _, clauses}, context} ->
{union(type, fun_from_non_overlapping_clauses(clauses)), fallback?, context}
{opt_union(type, fun_from_non_overlapping_clauses(clauses)), fallback?, context}
{{:infer, _, clauses}, context} when length(clauses) <= @max_clauses ->
{union(type, fun_from_inferred_clauses(clauses)), fallback?, context}
{opt_union(type, fun_from_inferred_clauses(clauses)), fallback?, context}
{_, context} ->
{type, true, context}
@@ -1738,7 +1766,7 @@ defmodule Module.Types.Apply do
defp map_put_new(map, key, value, name, args_types, stack) do
fun = fn
true, type -> union(type, value)
true, type -> opt_union(type, value)
false, type -> if empty?(type), do: value, else: type
end
@@ -1794,7 +1822,7 @@ defmodule Module.Types.Apply do
case filter_domain(clauses, expected, [], true) do
:none -> domain(domain, clauses)
:all -> domain(domain, clauses)
args -> Enum.zip_with(args, fn types -> Enum.reduce(types, &union/2) end)
args -> Enum.zip_with(args, fn types -> Enum.reduce(types, &opt_union/2) end)
end
end
@@ -1818,7 +1846,7 @@ defmodule Module.Types.Apply do
{used, dynamic()}
{_count, used, returns} ->
{used, returns |> Enum.reduce(&union/2) |> dynamic()}
{used, returns |> Enum.reduce(&opt_union/2) |> dynamic()}
end
end
@@ -1835,7 +1863,7 @@ defmodule Module.Types.Apply do
# as a non disjoint check. So we skip checking compatibility twice.
with true <- zip_compatible_or_only_gradual?(args_types, domain),
{count, used, returns} when count > 0 <- apply_clauses(clauses, args_types, 0, 0, [], []) do
{used, returns |> Enum.reduce(&union/2) |> return(args_types, stack)}
{used, returns |> Enum.reduce(&opt_union/2) |> return(args_types, stack)}
else
_ -> :error
end
@@ -2180,7 +2208,7 @@ defmodule Module.Types.Apply do
clauses =
case mod.__protocol__(:impls) do
{:consolidated, mods} ->
domain = mods |> Enum.map(&Module.Types.Of.impl/1) |> Enum.reduce(&union/2)
domain = mods |> Enum.map(&Module.Types.Of.impl/1) |> Enum.reduce(&opt_union/2)
[{[domain], dynamic()}]
_ ->
@@ -2390,10 +2418,10 @@ defmodule Module.Types.Apply do
term_type?(actual) do
actual |> to_quoted() |> Code.Formatter.to_algebra()
else
common = intersection(actual, expected)
common = opt_intersection(actual, expected)
uncommon_doc =
difference(actual, expected)
opt_difference(actual, expected)
|> to_quoted()
|> Code.Formatter.to_algebra()
|> ansi_red()
@@ -2411,17 +2439,17 @@ defmodule Module.Types.Apply do
end
@composite_types non_empty_list(term(), term())
|> union(tuple())
|> union(open_map())
|> union(fun())
|> opt_union(tuple())
|> opt_union(open_map())
|> opt_union(fun())
# If actual/expected have a composite type, computing the
# `intersection(actual, expected) or difference(actual, expected)`
# `opt_intersection(actual, expected) or opt_difference(actual, expected)`
# can lead to an explosion of terms that actually make debugging
# harder. So we check that at least one of the two operations
# return none() (i.e. actual is a subtype or they are disjoint).
defp has_simple_difference?(actual, expected) do
composite_types = intersection(actual, @composite_types)
composite_types = opt_intersection(actual, @composite_types)
subtype?(composite_types, expected) or disjoint?(composite_types, expected)
end
File diff suppressed because it is too large Load Diff
+55 -43
View File
@@ -22,9 +22,9 @@ defmodule Module.Types.Expr do
context: atom([:match, :guard, nil]),
context_modules: list_of_modules,
file: binary(),
function: union(tuple([atom(), integer()]), atom([nil])),
function: opt_union(tuple([atom(), integer()]), atom([nil])),
functions: functions_and_macros,
lexical_tracker: union(pid(), atom([nil])),
lexical_tracker: opt_union(pid(), atom([nil])),
line: integer(),
macro_aliases: aliases,
macros: functions_and_macros,
@@ -38,24 +38,24 @@ defmodule Module.Types.Expr do
# then we will assume you need to statically handle all possible exceptions.
@exception open_map(__struct__: atom(), __exception__: term())
args_or_arity = union(list(term()), integer())
args_or_arity = opt_union(list(term()), integer())
extra_info =
list(
tuple([atom([:file]), list(integer())])
|> union(tuple([atom([:line]), integer()]))
|> union(tuple([atom([:error_info]), open_map()]))
|> opt_union(tuple([atom([:line]), integer()]))
|> opt_union(tuple([atom([:error_info]), open_map()]))
)
@stacktrace list(
union(
opt_union(
tuple([atom(), atom(), args_or_arity, extra_info]),
tuple([fun(), args_or_arity, extra_info])
)
)
@falsy atom([false, nil])
@truthy negation(atom([false, nil]))
@truthy Module.Types.Descr.opt_negation(atom([false, nil]))
# :atom
def of_expr(atom, _expected, _expr, _stack, context) when is_atom(atom),
@@ -106,7 +106,7 @@ defmodule Module.Types.Expr do
of_expr(suffix, tl_type, expr, stack, context)
end
{non_empty_list(Enum.reduce(prefix, &union/2), suffix), context}
{non_empty_list(Enum.reduce(prefix, &opt_union/2), suffix), context}
end
# {left, right}
@@ -150,13 +150,13 @@ defmodule Module.Types.Expr do
fn pattern_type, context ->
# See if we can use the expected type to further refine the pattern type,
# if we cannot, use the pattern type as that will fail later on.
type = intersection(pattern_type, expected)
type = opt_intersection(pattern_type, expected)
type = if empty?(type), do: pattern_type, else: type
{result, context} = of_expr(right_expr, type, expr, stack, context)
# The function may still return a too broad type,
# so we refine once again to assign the most appropriate to the pattern.
{intersection(result, expected), context}
{opt_intersection(result, expected), context}
end
Pattern.of_match(left_expr, type_fun, match, meta, stack, context)
@@ -187,7 +187,7 @@ defmodule Module.Types.Expr do
end
end)
expected = intersection(expected, open_map(expected_pairs))
expected = opt_intersection(expected, open_map(expected_pairs))
{map_type, context} = of_expr(map, expected, expr, stack, context)
try do
@@ -311,7 +311,7 @@ defmodule Module.Types.Expr do
maybe_always_or_never_match_cond(pos_head_type, pos_head, pos_meta, stack, context, false)
{_, truthy_context} =
of_expr(pos_head, @truthy, pos_head, %{stack | reverse_arrow: :use}, context)
of_expr(pos_head, @truthy, pos_head, %{stack | reverse_arrow: :except_none}, context)
# Keep the context except the warnings, and compute the body
truthy_context = reset_warnings(truthy_context, context)
@@ -323,14 +323,14 @@ defmodule Module.Types.Expr do
context = Of.reset_vars(pos_body_context, context)
{_, falsy_context} =
of_expr(pos_head, @falsy, pos_head, %{stack | reverse_arrow: :use}, context)
of_expr(pos_head, @falsy, pos_head, %{stack | reverse_arrow: :except_none}, context)
falsy_context = reset_warnings(falsy_context, context)
{neg_body_type, neg_body_context} =
of_expr(neg_body, expected, expr, stack, falsy_context)
body_type = union(pos_body_type, neg_body_type)
body_type = opt_union(pos_body_type, neg_body_type)
context =
cond do
@@ -355,7 +355,7 @@ defmodule Module.Types.Expr do
maybe_always_or_never_match_cond(head_type, head, meta, stack, context, last?)
{_, truthy_context} =
of_expr(head, @truthy, head, %{stack | reverse_arrow: :use}, context)
of_expr(head, @truthy, head, %{stack | reverse_arrow: :except_none}, context)
# Keep the context except the warnings, and compute the body
truthy_context = reset_warnings(truthy_context, context)
@@ -369,12 +369,12 @@ defmodule Module.Types.Expr do
context
else
{_, falsy_context} =
of_expr(head, @falsy, head, %{stack | reverse_arrow: :use}, context)
of_expr(head, @falsy, head, %{stack | reverse_arrow: :except_none}, context)
reset_warnings(falsy_context, context)
end
{union(body_type, acc), context}
{opt_union(body_type, acc), context}
end)
dynamic_unless_static({body_type, Of.reset_vars(acc_context, context)}, stack)
@@ -382,7 +382,7 @@ defmodule Module.Types.Expr do
end
def of_expr({:case, meta, [case_expr, [do: _clauses]]}, expected, _expr, stack, context)
when stack.reverse_arrow == :use do
when stack.reverse_arrow in [:except_none, :include_none] do
version = Keyword.fetch!(meta, :version)
clauses = Map.fetch!(context.reverse_arrows, version)
original = context
@@ -402,7 +402,7 @@ defmodule Module.Types.Expr do
context
# If there is a single clause, we assume it is always evaluated
# by doing reverse arrows and incorporting all variables into the context.
# by doing reverse arrows and incorporating all variables into the context.
# We have to evaluate the head again but it might have been preferred
# if we could somehow merge a previously computed context.
{_, [{arg_type, _body_type, {:->, meta, [head, body]}}]} ->
@@ -419,7 +419,7 @@ defmodule Module.Types.Expr do
reset_warnings(context, original)
{_, filtered} ->
case_expected = Enum.reduce(filtered, none(), &union(elem(&1, 0), &2))
case_expected = Enum.reduce(filtered, none(), &opt_union(elem(&1, 0), &2))
{_, context} = of_expr(case_expr, case_expected, case_expr, stack, context)
reset_warnings(context, original)
end
@@ -461,7 +461,13 @@ defmodule Module.Types.Expr do
# Now we refine the case_expr context and use it to compute the body
{_, refined_context} =
of_expr(case_expr, arg_type, case_expr, %{stack | reverse_arrow: :use}, context)
of_expr(
case_expr,
arg_type,
case_expr,
%{stack | reverse_arrow: :except_none},
context
)
{body_type, context} =
of_expr(body, expected, body, stack, reset_warnings(refined_context, context))
@@ -474,16 +480,22 @@ defmodule Module.Types.Expr do
else
[arg_type] = Pattern.of_domain(trees, stack, context)
clauses_acc = [{arg_type, body_type, clause} | clauses_acc]
{{none?, union(body_type, body_acc), clauses_acc}, context}
{{none?, opt_union(body_type, body_acc), clauses_acc}, context}
end
end)
context =
if none? or stack.mode != :static do
head_type = Enum.reduce(clauses_acc, none(), &union(elem(&1, 0), &2))
head_type = Enum.reduce(clauses_acc, none(), &opt_union(elem(&1, 0), &2))
{_, refined_context} =
of_expr(case_expr, head_type, case_expr, %{stack | reverse_arrow: :use}, context)
of_expr(
case_expr,
head_type,
case_expr,
%{stack | reverse_arrow: :except_none},
context
)
reset_warnings(refined_context, context)
else
@@ -546,7 +558,7 @@ defmodule Module.Types.Expr do
end
{type, context} = of_expr(body, expected, body, stack, context)
{union(type, acc), context |> set_failed(failed?) |> Of.reset_vars(original)}
{opt_union(type, acc), context |> set_failed(failed?) |> Of.reset_vars(original)}
end)
{:catch, clauses}, {acc, context} ->
@@ -564,7 +576,7 @@ defmodule Module.Types.Expr do
end)
end
@timeout_type union(integer(), atom([:infinity]))
@timeout_type opt_union(integer(), atom([:infinity]))
def of_expr({:receive, meta, [blocks]}, expected, expr, stack, original) do
cache_result(meta, stack, original, fn ->
@@ -581,10 +593,11 @@ defmodule Module.Types.Expr do
{body_type, context} = of_expr(body, expected, expr, stack, context)
if compatible?(timeout_type, @timeout_type) do
{union(body_type, acc), Of.reset_vars(context, original)}
{opt_union(body_type, acc), Of.reset_vars(context, original)}
else
error = {:badtimeout, timeout_type, timeout, context}
{union(body_type, acc), error(__MODULE__, error, meta, stack, context)}
context = error(__MODULE__, error, meta, stack, context)
{opt_union(body_type, acc), Of.reset_vars(context, original)}
end
end)
|> dynamic_unless_static(stack)
@@ -613,13 +626,13 @@ defmodule Module.Types.Expr do
case into_kind do
:bitstring ->
{block_type, context} = of_expr(block, bitstring(), block, stack, context)
intersection = intersection(block_type, bitstring())
intersection = opt_intersection(block_type, bitstring())
if empty?(intersection) do
error = {:badbitbody, block_type, block, context}
{error_type(), error(__MODULE__, error, meta, stack, context)}
else
{union(into_type, intersection), context}
{opt_union(into_type, intersection), context}
end
:non_empty_list ->
@@ -630,7 +643,7 @@ defmodule Module.Types.Expr do
end
{block_type, context} = of_expr(block, expected, block, stack, context)
{union(into_type, non_empty_list(block_type)), context}
{opt_union(into_type, non_empty_list(block_type)), context}
:none ->
{_, context} = of_expr(block, term(), block, stack, context)
@@ -641,7 +654,6 @@ defmodule Module.Types.Expr do
end)
end
# TODO: with pat <- expr do expr end
def of_expr({:with, meta, [_ | _] = clauses}, expected, _expr, stack, original) do
cache_result(meta, stack, original, fn ->
{clauses, [[do: do_block] ++ options]} = Enum.split(clauses, -1)
@@ -662,7 +674,7 @@ defmodule Module.Types.Expr do
{else_types, context}
end
dynamic_unless_static({union(do_result, else_result), context}, stack)
dynamic_unless_static({opt_union(do_result, else_result), context}, stack)
end)
end
@@ -775,7 +787,7 @@ defmodule Module.Types.Expr do
context
_ ->
expected = if structs == [], do: @exception, else: Enum.reduce(structs, &union/2)
expected = if structs == [], do: @exception, else: Enum.reduce(structs, &opt_union/2)
expr = {:__block__, [type_check: info], [expr]}
context = Of.declare_var(var, context)
{_ok?, _type, context} = Of.refine_head_var(var, expected, expr, stack, context)
@@ -819,7 +831,7 @@ defmodule Module.Types.Expr do
context
end
@into_compile union(bitstring(), empty_list())
@into_compile opt_union(bitstring(), empty_list())
defp for_into([], _meta, _stack, context),
do: {empty_list(), :non_empty_list, context}
@@ -848,7 +860,7 @@ defmodule Module.Types.Expr do
# If they can be both be true, then we don't know
# what the contents of the block are for
{true, true} ->
type = union(bitstring(), list(term()))
type = opt_union(bitstring(), list(term()))
{if(gradual?(type), do: dynamic(type), else: type), :none, context}
{false, true} ->
@@ -879,10 +891,10 @@ defmodule Module.Types.Expr do
[pattern_type] = Pattern.of_domain(trees, stack, context)
{_, refined_context} =
of_expr(right, pattern_type, right, %{stack | reverse_arrow: :use}, context)
of_expr(right, pattern_type, right, %{stack | reverse_arrow: :except_none}, context)
else_type = if precise?, do: difference(type, pattern_type), else: type
{union(else_type, else_types), reset_warnings(refined_context, context)}
else_type = if precise?, do: opt_difference(type, pattern_type), else: type
{opt_union(else_type, else_types), reset_warnings(refined_context, context)}
end
defp with_clause(expr, stack, {else_types, context}) do
@@ -906,7 +918,7 @@ defmodule Module.Types.Expr do
Of.with_conditional_vars(mods, none(), call, stack, context, fn mod, acc, context ->
expr = {remote, [type_check: {:invoked_as, mod, fun, length(args)}] ++ meta, args}
{type, context} = Apply.remote(mod, fun, args, expected, expr, stack, context, &of_expr/5)
{union(acc, type), context}
{opt_union(acc, type), context}
end)
end
@@ -949,7 +961,7 @@ defmodule Module.Types.Expr do
context = put_in(context.reverse_arrows[version], result)
{result, context}
:use ->
_ ->
version = Keyword.fetch!(meta, :version)
{Map.fetch!(context.reverse_arrows, version), context}
end
@@ -970,7 +982,7 @@ defmodule Module.Types.Expr do
defp of_clauses(clauses, domain, expected, base_info, stack, context, acc) do
of_acc = fn _args_types, _precise?, {:->, _, [_, body]}, context, acc ->
{body_type, context} = of_expr(body, expected, body, stack, context)
{union(acc, body_type), context}
{opt_union(acc, body_type), context}
end
of_clauses_fun(clauses, domain, base_info, stack, context, acc, of_acc)
@@ -1025,7 +1037,7 @@ defmodule Module.Types.Expr do
do: {left_expr, right_expr}
defp add_inferred([{args, existing_return} | tail], args, return),
do: [{args, union(existing_return, return)} | tail]
do: [{args, opt_union(existing_return, return)} | tail]
defp add_inferred([head | tail], args, return),
do: [head | add_inferred(tail, args, return)]
+2 -2
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
@@ -329,7 +329,7 @@ defmodule Module.Types.Helpers do
end
end
{{:., _, [:lists, :member]}, meta, [expr, [_ | _] = args]} = call ->
{{:., _, [:lists, :member]}, meta, [expr, args]} = call when is_list(args) ->
if Enum.any?(args, &match?({:|, _, [_, _]}, &1)) do
call
else
+53 -43
View File
@@ -11,7 +11,7 @@ defmodule Module.Types.Of do
@prefix quote(do: ...)
@suffix quote(do: ...)
@integer_or_float union(integer(), float())
@integer_or_float opt_union(integer(), float())
@integer integer()
@float float()
@binary binary()
@@ -31,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
@@ -106,15 +106,15 @@ defmodule Module.Types.Of do
context
end
case context do
case stack do
_ when type in @skip_refinement_for or is_map_key(data, :errored) ->
{old_type, context}
%{pattern_info: %{guard_context: guard_context}} ->
new_type = intersection(old_type, type)
%{reverse_arrow: reverse_arrow} when reverse_arrow in [:except_none, :include_none] ->
new_type = opt_intersection(old_type, type)
case empty?(new_type) do
true when guard_context == :orelse ->
true when reverse_arrow == :include_none ->
data = %{
data
| type: none(),
@@ -171,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
@@ -245,7 +245,7 @@ defmodule Module.Types.Of do
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)
@@ -264,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__: if_set(Module.Types.Descr.opt_negation(atom())))},
{Port, port()},
{PID, pid()},
{Reference, reference()},
@@ -384,15 +384,18 @@ defmodule Module.Types.Of do
for key <- keys, t <- cartesian_map(tail) do
closed_map(non_multiple ++ [{key, type} | t])
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} -> {key, opt_union(old_type, new_type)} end)
multiple =
Enum.map(multiple, fn {keys, old_type} -> {keys, opt_union(old_type, new_type)} end)
{single, multiple}
end
@@ -402,13 +405,13 @@ defmodule Module.Types.Of do
if key in negated do
{{key, old_type}, [key | matched]}
else
{{key, union(old_type, new_type)}, matched}
{{key, opt_union(old_type, new_type)}, 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}
@@ -467,32 +470,32 @@ defmodule Module.Types.Of do
# 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, :expr, meta, stack, context)
{info, context} = struct_info(struct, :expr, meta, stack, context, true)
if is_nil(info) do
raise "expected #{inspect(struct)} to return struct metadata, but got none"
{dynamic(), context}
else
# The compiler has already checked the keys are atoms and which ones are required.
{args_types, context} =
Enum.map_reduce(args, context, fn {key, value}, context when is_atom(key) ->
value_type =
case map_fetch_key(expected, key) do
{_, expected_value_type} -> expected_value_type
_ -> term()
end
{type, context} = of_fun.(value, value_type, stack, context)
{{key, type}, context}
end)
{closed_map([{:__struct__, atom([struct])} | args_types]), context}
end
# The compiler has already checked the keys are atoms and which ones are required.
{args_types, context} =
Enum.map_reduce(args, context, fn {key, value}, context when is_atom(key) ->
value_type =
case map_fetch_key(expected, key) do
{_, expected_value_type} -> expected_value_type
_ -> term()
end
{type, context} = of_fun.(value, value_type, stack, context)
{{key, type}, context}
end)
{closed_map([{:__struct__, atom([struct])} | args_types]), context}
end
@doc """
Returns `__info__(:struct)` information about a struct.
"""
def struct_info(struct, kind, meta, stack, context) 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, :soft, env) do
@@ -511,7 +514,7 @@ defmodule Module.Types.Of do
{info, context}
else
error = {:unknown_struct, kind, struct}
error = {:unknown_struct, kind, struct, must_exist?}
{nil, error(error, meta, stack, context)}
end
end
@@ -848,19 +851,26 @@ defmodule Module.Types.Of do
}
end
def format_diagnostic({:unknown_struct, kind, module}) do
message =
if Code.ensure_loaded?(module) do
"struct #{inspect(module)} is undefined (there is such module but it does not define a struct)"
else
"struct #{inspect(module)} is undefined " <>
"(module #{inspect(module)} is not available or is yet to be defined)"
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: message,
message: "struct #{inspect(module)} is undefined (#{detail})",
group: true,
severity: if(kind == :pattern, do: :error, else: :warning)
severity: if(kind == :pattern or must_exist?, do: :error, else: :warning)
}
end
+55 -27
View File
@@ -27,7 +27,7 @@ defmodule Module.Types.Pattern do
do: previous
defp concat_previous(types, {list, descr}),
do: {[types | list], union(descr, args_to_previous(types))}
do: {[types | list], opt_union(descr, args_to_previous(types))}
defp args_to_previous([type]), do: upper_bound(type)
defp args_to_previous(types), do: args_to_static_domain(types)
@@ -39,8 +39,8 @@ defmodule Module.Types.Pattern do
defp of_pattern_previous(types, {_, descr}, stack) do
refined_types =
case types do
[type] -> [difference(type, descr)]
[_ | _] -> args_to_domain(types) |> difference(descr) |> domain_to_flat_args(types)
[type] -> [opt_difference(type, descr)]
[_ | _] -> args_to_domain(types) |> opt_difference(descr) |> domain_to_flat_args(types)
end
# check_previous? is an optimization. If types have not changed,
@@ -349,7 +349,7 @@ defmodule Module.Types.Pattern do
defp of_pattern_intersect([head | tail], index, acc, pattern_info, tag, stack, context) do
{tree, expected, pattern} = head
actual = of_pattern_tree(tree, stack, context)
type = intersection(actual, expected)
type = opt_intersection(actual, expected)
if empty?(type) do
context = badpattern_error(pattern, index, tag, stack, context)
@@ -498,7 +498,7 @@ defmodule Module.Types.Pattern do
defp of_pattern_var([{:subpattern, key} | rest], type, context) do
%{^key => subpattern} = context.subpatterns
of_pattern_var(rest, intersection(type, subpattern), context)
of_pattern_var(rest, opt_intersection(type, subpattern), context)
end
defp of_pattern_var([:tail | rest], type, context) do
@@ -518,7 +518,7 @@ defmodule Module.Types.Pattern do
# This logic mirrors the code in `Apply.compare`
cond do
# If it is a singleton, we can always be precise
singleton?(type) -> if polarity, do: type, else: negation(type)
singleton?(type) -> if polarity, do: type, else: Module.Types.Descr.opt_negation(type)
# We are checking for `not x == 1` or similar, we can't say anything about x
polarity == false -> term()
# We are checking for `x == 1`, make sure x is integer or float
@@ -550,7 +550,7 @@ defmodule Module.Types.Pattern do
tail
|> Enum.reduce(
of_pattern_tree(head, stack, context),
&union(of_pattern_tree(&1, stack, context), &2)
&opt_union(of_pattern_tree(&1, stack, context), &2)
)
|> non_empty_list(of_pattern_tree(suffix, stack, context))
end
@@ -558,7 +558,7 @@ defmodule Module.Types.Pattern do
defp of_pattern_tree({:intersection, entries}, stack, context) do
entries
|> Enum.map(&of_pattern_tree(&1, stack, context))
|> Enum.reduce(&intersection/2)
|> Enum.reduce(&opt_intersection/2)
end
defp of_pattern_tree({:var, version}, _stack, context) do
@@ -690,7 +690,7 @@ defmodule Module.Types.Pattern do
if static == [] do
{:intersection, dynamic}
else
{:intersection, [Enum.reduce(static, &intersection/2) | dynamic]}
{:intersection, [Enum.reduce(static, &opt_intersection/2) | dynamic]}
end
context = of_var(var, version, [%{root: return, expr: match}], context)
@@ -786,8 +786,9 @@ defmodule Module.Types.Pattern do
# <<...>>>
defp of_pattern({:<<>>, _meta, args} = node, _path, stack, context) do
precise? = of_precise_bitstring?(node, context.vars)
{type, context} = Of.bitstring(args, :match, stack, context)
{type, of_precise_bitstring?(node), context}
{type, precise?, context}
end
# left ++ right
@@ -950,13 +951,13 @@ defmodule Module.Types.Pattern do
type =
case {static, dynamic} do
{static, []} when is_descr(suffix_type) ->
non_empty_list(Enum.reduce(static, &union/2), suffix_type)
non_empty_list(Enum.reduce(static, &opt_union/2), suffix_type)
{[], dynamic} ->
{:non_empty_list, dynamic, suffix_type}
{static, dynamic} ->
{:non_empty_list, [Enum.reduce(static, &union/2) | dynamic], suffix_type}
{:non_empty_list, [Enum.reduce(static, &opt_union/2) | dynamic], suffix_type}
end
{type, false, context}
@@ -987,12 +988,19 @@ defmodule Module.Types.Pattern do
{type, precise?, put_in(context.subpatterns[key], of_pattern_tree(type, stack, context))}
end
defp of_precise_bitstring?({:<<>>, _meta, [{:"::", _, [expr, {type, _, _}]}]})
defp of_precise_bitstring?({:<<>>, _meta, [{:"::", _, [expr, {type, _, _}]}]}, vars)
when type in [:binary, :bitstring, :bytes, :bits] do
is_var(expr) or of_precise_bitstring?(expr)
new_var?(expr, vars) or of_precise_bitstring?(expr, vars)
end
defp of_precise_bitstring?(_), do: false
defp of_precise_bitstring?(_expr, _vars), do: false
defp new_var?({_, meta, _} = var, vars) when is_var(var) do
version = Keyword.fetch!(meta, :version)
not is_map_key(vars, version)
end
defp new_var?(_expr, _vars), do: false
## Guards
#
@@ -1018,9 +1026,10 @@ defmodule Module.Types.Pattern do
end
defp of_guards(guards, vars, stack, context) do
stack = %{stack | reverse_arrow: :except_none}
context =
init_pattern_info(context, %{
guard_context: :andalso,
parent_version: nil,
vars: vars,
subpatterns_vars: %{},
@@ -1065,10 +1074,6 @@ defmodule Module.Types.Pattern do
%{context | pattern_info: %{pattern_info | parent_version: parent_version}}}
end
defp enable_conditional_mode(%{pattern_info: pattern_info} = context) do
%{context | pattern_info: %{pattern_info | guard_context: :orelse}, conditional_vars: %{}}
end
defp maybe_badguard(type, guard, stack, context) do
case booleaness(type) do
:maybe_both ->
@@ -1120,7 +1125,7 @@ defmodule Module.Types.Pattern do
Enum.map_reduce(prefix, context, &of_guard(&1, term(), expr, stack, &2))
{suffix, context} = of_guard(suffix, term(), expr, stack, context)
{non_empty_list(Enum.reduce(prefix, &union/2), suffix), context}
{non_empty_list(Enum.reduce(prefix, &opt_union/2), suffix), context}
end
# {left, right}
@@ -1261,7 +1266,7 @@ defmodule Module.Types.Pattern do
# `:fail` as dynamic as its whole purpose is to cause failures.
defp of_remote(:orelse, [left, :fail], _call, expected, stack, context) do
{type, context} = of_guard(left, expected, left, stack, context)
{union(type, @dynamic_fail), context}
{opt_union(type, @dynamic_fail), context}
end
defp of_remote(fun, _args, call, expected, stack, context)
@@ -1294,7 +1299,8 @@ defmodule Module.Types.Pattern do
of_guard(left, expected, left, stack, context)
true ->
cond_context = enable_conditional_mode(context)
cond_context = %{context | conditional_vars: %{}}
stack = %{stack | reverse_arrow: :include_none}
# Compute the sure types, which are stored directly in the context
{_type, context} = of_guard(left, boolean(), left, stack, context)
@@ -1432,7 +1438,7 @@ defmodule Module.Types.Pattern do
case disjoint? do
true -> {type, context}
false -> {union(to_abort, type), context}
false -> {opt_union(to_abort, type), context}
end
end
@@ -1447,7 +1453,7 @@ defmodule Module.Types.Pattern do
of_guard(head, expected, head, stack, context)
acc_vars = [{vars, cond_vars} | acc_vars]
of_logical_cond(tail, union(acc_type, type), acc_vars, expected, stack, context)
of_logical_cond(tail, opt_union(acc_type, type), acc_vars, expected, stack, context)
end
defp of_logical_cond([], acc_type, acc_vars, _expected, _stack, _context) do
@@ -1564,7 +1570,18 @@ defmodule Module.Types.Pattern do
#{expr_to_string({:!, [], [expr]}) |> indent(4)}
will always evaluate to true because the expression has type:
will always evaluate to true because its inner expression has type:
#{to_quoted_string(type) |> indent(4)}
"""
{:case, :"!!"} ->
"""
the following conditional expression:
#{expr_to_string({:!, [], [{:!, [], [expr]}]}) |> indent(4)}
will always evaluate to false because its inner expression has type:
#{to_quoted_string(type) |> indent(4)}
"""
@@ -1721,7 +1738,18 @@ defmodule Module.Types.Pattern do
#{expr_to_string({:!, [], [expr]}) |> indent(4)}
will always evaluate to false because the expression has type:
will always evaluate to false because its inner expression has type:
#{to_quoted_string(type) |> indent(4)}
"""
op == :"!!" ->
"""
the following conditional expression:
#{expr_to_string({:!, [], [{:!, [], [expr]}]}) |> indent(4)}
will always evaluate to true because its inner expression has type:
#{to_quoted_string(type) |> indent(4)}
"""
+1 -1
View File
@@ -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
+6 -4
View File
@@ -282,7 +282,9 @@ defmodule Protocol do
type_args = :lists.map(fn _ -> quote(do: term) end, :lists.seq(2, arity))
type_args = [quote(do: t) | type_args]
to_var = fn pos -> Macro.var(String.to_atom("arg" <> Integer.to_string(pos)), __MODULE__) end
to_var = fn pos ->
Macro.var(String.to_unsafe_atom("arg" <> Integer.to_string(pos)), __MODULE__)
end
call_args = :lists.map(to_var, :lists.seq(2, arity))
call_args = [quote(do: term) | call_args]
@@ -694,10 +696,10 @@ defmodule Protocol do
domain =
Enum.reduce(types_minus_any -- structs, structs_domain, fn impl, acc ->
Descr.union(Module.Types.Of.impl(impl, :open), acc)
Descr.opt_union(Module.Types.Of.impl(impl, :open), acc)
end)
not_domain = Descr.negation(domain)
not_domain = Descr.opt_negation(domain)
if Any in types do
clauses =
@@ -1228,7 +1230,7 @@ defmodule Protocol do
end
def __concat__(left, right) when is_binary(right) do
String.to_atom(ensure_prefix(Atom.to_string(left)) <> "." <> right)
String.to_unsafe_atom(ensure_prefix(Atom.to_string(left)) <> "." <> right)
end
defp ensure_prefix("Elixir." <> _ = left), do: left
+1 -1
View File
@@ -40,7 +40,7 @@ defmodule Record.Extractor do
defp from_lib_file(file) do
[app | path] = :filename.split(String.to_charlist(file))
case :code.lib_dir(List.to_atom(app)) do
case :code.lib_dir(List.to_unsafe_atom(app)) do
{:error, _} ->
raise ArgumentError, "lib file #{file} could not be found"
+67 -33
View File
@@ -21,8 +21,8 @@ defmodule Registry do
the `:via` option), storing properties, custom dispatching rules, or a pubsub
implementation. We explore some of those use cases below.
The registry may also be transparently partitioned, which provides
more scalable behavior for running registries on highly concurrent
The registry may also be transparently partitioned with the `:partitions` option,
which provides more scalable behavior for running registries on highly concurrent
environments with thousands or millions of entries.
## Using in `:via`
@@ -1127,22 +1127,26 @@ defmodule Registry do
end
defp unregister_match_specs({:duplicate, :key}, key, self, pattern, guards) do
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
if exact_match_key?(key) do
{[{{{key, self, :_}, :_}, [], [true]}], [{{{key, self, :_}, pattern}, guards, [true]}]}
else
guard = {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}}
pid_guard = {:"=:=", {:element, 2, {:element, 1, :"$_"}}, {:const, self}}
{[{{{:_, :_, :_}, :_}, [guard, pid_guard], [true]}],
[{{{:_, :_, :_}, pattern}, [guard, pid_guard | guards], [true]}]}
else
{[{{{key, self, :_}, :_}, [], [true]}], [{{{key, self, :_}, pattern}, guards, [true]}]}
end
end
defp unregister_match_specs(_kind, key, self, pattern, guards) do
underscore_guard = {:"=:=", {:element, 1, :"$_"}, {:const, key}}
if exact_match_key?(key) do
{[{{key, {self, :_}}, [], [true]}], [{{key, {self, pattern}}, guards, [true]}]}
else
underscore_guard = {:"=:=", {:element, 1, :"$_"}, {:const, key}}
{[{{:_, {self, :_}}, [underscore_guard], [true]}],
[{{:_, {self, pattern}}, [underscore_guard | guards], [true]}]}
{[{{:_, {self, :_}}, [underscore_guard], [true]}],
[{{:_, {self, pattern}}, [underscore_guard | guards], [true]}]}
end
end
@doc """
@@ -1484,8 +1488,9 @@ defmodule Registry do
that reference the underlying entry structure via `:"$_"` will return different results.
Use named variables like `:"$1"`, `:"$2"`, `:"$3"` instead.
Note that for large registries with many partitions this will be costly as it builds the result by
concatenating all the partitions.
Note that for registries with many partitions, the result of each partition is concatenated.
Therefore selecting thousands of entries across partitions may become an expensive operation
and we recommend profiling/benchmarking whenever selecting too many entries.
## Examples
@@ -1618,11 +1623,11 @@ defmodule Registry do
defp lookup_second({:duplicate, :key}, ets, key) do
spec =
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
if exact_match_key?(key) do
[{{{key, :"$1", :_}, :"$2"}, [], [{{:"$1", :"$2"}}]}]
else
guard = {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}}
[{{{:_, :"$1", :_}, :"$2"}, [guard], [{{:"$1", :"$2"}}]}]
else
[{{{key, :"$1", :_}, :"$2"}, [], [{{:"$1", :"$2"}}]}]
end
try do
@@ -1664,25 +1669,24 @@ defmodule Registry do
def __unregister__(table, match, pos, ordered \\ false)
def __unregister__(table, {key, {pid, :_}}, 1, true = _ordered) do
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
if exact_match_key?(key) do
:ets.match_delete(table, {{key, pid, :_}, :_})
else
guard = {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}}
pid_guard = {:"=:=", {:element, 2, {:element, 1, :"$_"}}, {:const, pid}}
:ets.select_delete(table, [{{:_, :_}, [guard, pid_guard], [true]}]) >= 0
else
:ets.match_delete(table, {{key, pid, :_}, :_})
end
end
def __unregister__(table, match, pos, false = _ordered) do
key = :erlang.element(pos, match)
# We need to perform an element comparison if we have a special atom key.
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
if exact_match_key?(key) do
:ets.match_delete(table, match)
else
match = :erlang.setelement(pos, match, :_)
guard = {:"=:=", {:element, pos, :"$_"}, {:const, key}}
:ets.select_delete(table, [{match, [guard], [true]}]) >= 0
else
:ets.match_delete(table, match)
end
end
@@ -1693,42 +1697,72 @@ defmodule Registry do
defp match_spec({:duplicate, :key}, key, pattern, guards) do
body = [{{{:element, 2, {:element, 1, :"$_"}}, {:element, 2, :"$_"}}}]
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
if exact_match_key?(key) do
[{{{key, :_, :_}, pattern}, guards, body}]
else
guards = [
{:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}} | guards
]
[{{{:_, :_, :_}, pattern}, guards, body}]
else
[{{{key, :_, :_}, pattern}, guards, body}]
end
end
defp match_spec(_kind, key, pattern, guards) do
guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards]
[{{:_, {:_, pattern}}, guards, [{:element, 2, :"$_"}]}]
if exact_match_key?(key) do
[{{key, {:_, pattern}}, guards, [{:element, 2, :"$_"}]}]
else
guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards]
[{{:_, {:_, pattern}}, guards, [{:element, 2, :"$_"}]}]
end
end
defp count_match_spec({:duplicate, :key}, key, pattern, guards) do
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
if exact_match_key?(key) do
[{{{key, :_, :_}, pattern}, guards, [true]}]
else
guards = [
{:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}} | guards
]
[{{{:_, :_, :_}, pattern}, guards, [true]}]
else
[{{{key, :_, :_}, pattern}, guards, [true]}]
end
end
defp count_match_spec(_kind, key, pattern, guards) do
guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards]
[{{:_, {:_, pattern}}, guards, [true]}]
if exact_match_key?(key) do
[{{key, {:_, pattern}}, guards, [true]}]
else
guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards]
[{{:_, {:_, pattern}}, guards, [true]}]
end
end
defp reserved_atom?("_"), do: true
defp reserved_atom?("$" <> _), do: true
defp reserved_atom?(_), do: false
# Only keys that remain exact when embedded in an ETS match head can use
# the optimized direct-key match path.
defp exact_match_key?(term) when is_map(term), do: false
defp exact_match_key?([]), do: true
defp exact_match_key?([head | tail]), do: exact_match_key?(head) and exact_match_key?(tail)
defp exact_match_key?(term) when is_tuple(term) do
exact_match_tuple?(term, tuple_size(term))
end
defp exact_match_key?(term) when is_atom(term) do
case Atom.to_string(term) do
"_" -> false
"$" <> _ -> false
_ -> true
end
end
defp exact_match_key?(_term), do: true
defp exact_match_tuple?(_term, 0), do: true
defp exact_match_tuple?(term, index) do
exact_match_key?(:erlang.element(index, term)) and exact_match_tuple?(term, index - 1)
end
defp ordered?({:duplicate, :key}), do: true
defp ordered?(_), do: false
+55 -5
View File
@@ -2962,6 +2962,12 @@ defmodule String do
end
end
# TODO: Deprecate String.to_atom, List.to_atom, and atom interpolation in v1.25
@doc deprecated: "Use to_existing_atom/1 or to_unsafe_atom/1 instead"
def to_atom(string) when is_binary(string) do
:erlang.binary_to_atom(string, :utf8)
end
@doc """
Converts a string to an existing atom or creates a new one.
@@ -2979,12 +2985,13 @@ defmodule String do
## Examples
iex> String.to_atom("my_atom")
iex> String.to_unsafe_atom("my_atom")
:my_atom
"""
@spec to_atom(String.t()) :: atom
def to_atom(string) when is_binary(string) do
@doc since: "1.21.0"
@spec to_unsafe_atom(String.t()) :: atom
def to_unsafe_atom(string) when is_binary(string) do
:erlang.binary_to_atom(string, :utf8)
end
@@ -2992,6 +2999,8 @@ defmodule String do
Converts a string to an existing atom or raises if
the atom does not exist.
If the list of expected atoms is known upfront, prefer `to_existing_atom/2`.
The maximum atom size is of 255 Unicode code points.
Raises an `ArgumentError` if the atom does not exist.
@@ -3021,6 +3030,47 @@ defmodule String do
:erlang.binary_to_existing_atom(string, :utf8)
end
@doc """
Converts a string 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> String.to_existing_atom("foo", [:foo, :bar])
:foo
iex> String.to_existing_atom("unknown", [:foo, :bar])
** (ArgumentError) unexpected value: \"unknown\", the allowed atoms are: [:foo, :bar]
"""
@doc since: "1.21.0"
@spec to_existing_atom(String.t(), nonempty_list(a)) :: a when a: atom()
def to_existing_atom(string, [_ | _] = allowed_atoms) when is_binary(string) do
atom = :erlang.binary_to_existing_atom(string, :utf8)
if atom not in allowed_atoms do
to_existing_atom_unexpected(string, allowed_atoms)
end
atom
end
# used just to have a less cryptic stacktrace and consistent error
@doc false
def __to_existing_atom__(string, allowed_atoms) do
to_existing_atom_unexpected(string, allowed_atoms)
end
defp to_existing_atom_unexpected(string, allowed_atoms) do
raise ArgumentError,
"unexpected value: #{inspect(string)}, the allowed atoms are: #{inspect(allowed_atoms)}"
end
@doc """
Returns an integer whose text representation is `string`.
@@ -3130,9 +3180,9 @@ defmodule String do
{bag2, length2} = string_to_bag(string2, %{}, 0)
diff1 = bag_difference(bag1, bag2)
diff2 = bag_difference(bag2, bag1)
max_diff = if length1 >= length2, do: diff1, else: diff1 - length1 + length2
1 - max(diff1, diff2) / max(length1, length2)
1 - max_diff / max(length1, length2)
end
defp string_to_bag(string, bag, length) do
+12 -1
View File
@@ -541,7 +541,7 @@ defmodule System do
By default, the Erlang VM register traps to the three
signals:
* `:sigstop` - gracefully shuts down the VM with `stop/0`
* `:sigterm` - gracefully shuts down the VM with `stop/0`
* `:sigquit` - halts the VM via `halt/0`
* `:sigusr1` - halts the VM via status code of 1
@@ -1378,6 +1378,17 @@ defmodule System do
@doc """
Returns the Erlang/OTP release number.
The version is a single number, the major version of OTP.
Erlang/OTP does not provide an API to fetch the exact
patch version, see the [*Versions* page](https://www.erlang.org/doc/system/versions.html)
in the Erlang documentation for more information.
## Examples
iex> System.otp_release()
"#{:erlang.system_info(:otp_release)}"
"""
@spec otp_release :: String.t()
@doc since: "1.3.0"
+36 -20
View File
@@ -24,6 +24,12 @@ defmodule URI do
[scheme]://[userinfo]@[host]:[port][path]?[query]#[fragment]
The fields contain the encoded URI components as they appear in the URI
itself. For example, a slash inside the userinfo must be stored as `%2F`,
not as `/`. Functions such as `parse/1` and `new/1` preserve existing
percent-encoded sequences in those fields, and functions such as `to_string/1`
expects those fields to already be encoded as needed. Whenever setting or
modifying the fields directly, you must encode them accordingly.
Note the `authority` field is deprecated. `parse/1` will still
populate it for backwards compatibility but you should generally
@@ -246,7 +252,7 @@ defmodule URI do
{{key, value}, rest} ->
# Avoid warnings about Dict being deprecated
dict_module = String.to_atom("Dict")
dict_module = String.to_unsafe_atom("Dict")
decode_query_into_dict(rest, dict_module.put(dict, key, value), encoding)
end
end
@@ -478,18 +484,19 @@ defmodule URI do
unpercent(string, "", true)
end
defguardp is_hex(char) when char in ?0..?9 or char in ?A..?F or char in ?a..?f
defp unpercent(<<?+, tail::binary>>, acc, spaces = true) do
unpercent(tail, <<acc::binary, ?\s>>, spaces)
end
defp unpercent(<<?%, h1, h2, tail::binary>>, acc, spaces)
when is_hex(h1) and is_hex(h2) do
unpercent(tail, <<acc::binary, bsl(hex_to_dec(h1), 4) + hex_to_dec(h2)>>, spaces)
end
defp unpercent(<<?%, tail::binary>>, acc, spaces) do
with <<hex1, hex2, tail::binary>> <- tail,
dec1 when is_integer(dec1) <- hex_to_dec(hex1),
dec2 when is_integer(dec2) <- hex_to_dec(hex2) do
unpercent(tail, <<acc::binary, bsl(dec1, 4) + dec2>>, spaces)
else
_ -> unpercent(tail, <<acc::binary, ?%>>, spaces)
end
unpercent(tail, <<acc::binary, ?%>>, spaces)
end
defp unpercent(<<head, tail::binary>>, acc, spaces) do
@@ -502,7 +509,6 @@ defmodule URI do
defp hex_to_dec(n) when n in ?A..?F, do: n - ?A + 10
defp hex_to_dec(n) when n in ?a..?f, do: n - ?a + 10
defp hex_to_dec(n) when n in ?0..?9, do: n - ?0
defp hex_to_dec(_n), do: nil
@doc """
Creates a new URI struct by parsing and validating a string or from an existing URI.
@@ -897,6 +903,9 @@ defmodule URI do
@doc """
Returns the string representation of the given [URI struct](`t:t/0`).
This function assembles the URI components into a string, assuming each
field is valid and escaped as done by `parse/1` and `new/1`.
## Examples
iex> uri = URI.parse("http://google.com")
@@ -1081,15 +1090,18 @@ defimpl String.Chars, for: URI do
":path in URI must be empty or an absolute path if URL has a :host, got: #{inspect(uri)}"
end
def to_string(%{scheme: scheme, port: port, path: path, query: query, fragment: fragment} = uri) do
uri =
case scheme && URI.default_port(scheme) do
^port -> %{uri | port: nil}
_ -> uri
end
def to_string(%{
host: host,
authority: authority,
userinfo: userinfo,
scheme: scheme,
port: port,
path: path,
query: query,
fragment: fragment
}) do
# Based on https://tools.ietf.org/html/rfc3986#section-5.3
authority = extract_authority(uri)
authority = extract_authority(host, authority, scheme, userinfo, port)
IO.iodata_to_binary([
if(scheme, do: [scheme, ?:], else: []),
@@ -1100,19 +1112,23 @@ defimpl String.Chars, for: URI do
])
end
defp extract_authority(%{host: nil, authority: authority}) do
defp extract_authority(_host = nil, authority, _scheme, _userinfo, _port) do
authority
end
defp extract_authority(%{host: host, userinfo: userinfo, port: port}) do
defp extract_authority(host, _authority, scheme, userinfo, port) do
# According to the grammar at
# https://tools.ietf.org/html/rfc3986#appendix-A, a "host" can have a colon
# in it only if it's an IPv6 or "IPvFuture" address, so if there's a colon
# in the host we can safely surround it with [].
[
if(userinfo, do: [userinfo | "@"], else: []),
if(String.contains?(host, ":"), do: ["[", host | "]"], else: host),
if(port, do: [":" | Integer.to_string(port)], else: [])
if(port != nil and (is_nil(scheme) or URI.default_port(scheme) != port),
do: [":" | Integer.to_string(port)],
else: []
)
]
end
end
+18 -1
View File
@@ -18,12 +18,16 @@ defmodule Version do
MAJOR.MINOR.PATCH
Each numeric component is limited to at most 14 digits.
Pre-releases are supported by optionally appending a hyphen and a series of
period-separated identifiers immediately following the patch version.
Identifiers consist of only ASCII alphanumeric characters and hyphens (`[0-9A-Za-z-]`):
"1.0.0-alpha.3"
Numeric pre-release identifiers are also limited to at most 14 digits.
Build information can be added by appending a plus sign and a series of
dot-separated identifiers immediately following the patch or pre-release version.
Identifiers consist of only ASCII alphanumeric characters and hyphens (`[0-9A-Za-z-]`):
@@ -520,6 +524,8 @@ defmodule Version do
defmodule Parser do
@moduledoc false
@max_numeric_component_digits 14
operators = [
{">=", :>=},
{"<=", :<=},
@@ -621,7 +627,9 @@ defmodule Version do
defp require_digits(nil), do: :error
defp require_digits(string) do
if leading_zero?(string), do: :error, else: parse_digits(string, "")
if leading_zero?(string) or byte_size(string) > @max_numeric_component_digits,
do: :error,
else: parse_digits(string, "")
end
defp leading_zero?(<<?0, _, _::binary>>), do: true
@@ -649,6 +657,11 @@ defmodule Version do
end
end
defp convert_parts_to_integer([part | rest], acc)
when byte_size(part) > @max_numeric_component_digits do
if all_digits?(part), do: :error, else: convert_parts_to_integer(rest, [part | acc])
end
defp convert_parts_to_integer([part | rest], acc) do
case parse_digits(part, "") do
{:ok, integer} ->
@@ -667,6 +680,10 @@ defmodule Version do
{:ok, Enum.reverse(acc)}
end
defp all_digits?(<<char, rest::binary>>) when char in ?0..?9, do: all_digits?(rest)
defp all_digits?(<<>>), do: true
defp all_digits?(_other), do: false
defp valid_identifier?(<<char, rest::binary>>)
when char in ?0..?9
when char in ?a..?z
@@ -155,7 +155,7 @@ Picture yourself implementing code that converts string values into atoms. These
```elixir
defmodule MyRequestHandler do
def parse(%{"status" => status, "message" => message} = _payload) do
%{status: String.to_atom(status), message: message}
%{status: String.to_unsafe_atom(status), message: message}
end
end
```
@@ -165,75 +165,53 @@ iex> MyRequestHandler.parse(%{"status" => "ok", "message" => "all good"})
%{status: :ok, message: "all good"}
```
When we use the `String.to_atom/1` function to dynamically create an atom, it essentially gains potential access to create arbitrary atoms in our system, causing us to lose control over adhering to the limits established by the BEAM. This issue could be exploited by someone to create enough atoms to shut down a system.
When we use the `String.to_unsafe_atom/1` function to dynamically create an atom, it essentially gains potential access to create arbitrary atoms in our system, causing us to lose control over adhering to the limits established by the BEAM. This issue could be exploited by someone to create enough atoms to shut down a system.
#### Refactoring
To eliminate this anti-pattern, developers must either perform explicit conversions by mapping strings to atoms or replace the use of `String.to_atom/1` with `String.to_existing_atom/1`. An explicit conversion could be done as follows:
To eliminate this anti-pattern, developers must either:
* replace the use of `String.to_unsafe_atom/1` with a list of expected atoms in `String.to_existing_atom/2`
* use pattern-matching to map strings to their equivalent atoms
* use `String.to_existing_atom/1` when the former options are not possible
An explicit conversion could be done as follows:
```elixir
defmodule MyRequestHandler do
def parse(%{"status" => status, "message" => message} = _payload) do
%{status: convert_status(status), message: message}
end
defp convert_status("ok"), do: :ok
defp convert_status("error"), do: :error
defp convert_status("redirect"), do: :redirect
end
```
```elixir
iex> MyRequestHandler.parse(%{"status" => "status_not_seen_anywhere", "message" => "all good"})
** (FunctionClauseError) no function clause matching in MyRequestHandler.convert_status/1
```
By explicitly listing all supported statuses, you guarantee only a limited number of conversions may happen. Passing an invalid status will lead to a function clause error.
An alternative is to use `String.to_existing_atom/1`, which will only convert a string to atom if the atom already exists in the system:
```elixir
defmodule MyRequestHandler do
def parse(%{"status" => status, "message" => message} = _payload) do
%{status: String.to_existing_atom(status), message: message}
status = String.to_existing_atom(status, [:ok, :error, :redirect])
%{status: status, message: message}
end
end
```
This will only accept valid values and convert `"ok"` as `:ok`, `"error"` as `:error`, and `"redirect"` as `:redirect`. Any other value would result in an `ArgumentError`, either because the atom doesn't exist or because it isn't part of the list:
```elixir
iex> MyRequestHandler.parse(%{"status" => "status_not_seen_anywhere", "message" => "all good"})
** (ArgumentError) errors were found at the given arguments:
* 1st argument: not an already existing atom
** (ArgumentError) ...
```
In such cases, passing an unknown status will raise as long as the status was not defined anywhere as an atom in the system. However, assuming `status` can be either `:ok`, `:error`, or `:redirect`, how can you guarantee those atoms exist? You must ensure those atoms exist somewhere **in the same module** where `String.to_existing_atom/1` is called. For example, if you had this code:
By explicitly listing all supported statuses, you guarantee that only a limited number of conversions may happen, and that all expected atoms already exist within the system.
An alternative is to use pattern matching and explicitly convert each string to its respective atom. This is useful when you need additional logic on differnt branches:
```elixir
defmodule MyRequestHandler do
def parse(%{"status" => status, "message" => message} = _payload) do
%{status: String.to_existing_atom(status), message: message}
end
def handle(%{status: status}) do
case status do
:ok -> ...
:error -> ...
:redirect -> ...
"ok" -> %{status: :ok, message: message}
"error" -> %{status: :error, message: message}
"redirect" -> %{status: :redirect, message: message, code: 302}
end
end
end
```
All valid statuses are defined as atoms within the same module, and that's enough. If you want to be explicit, you could also have a function that lists them:
```elixir
def valid_statuses do
[:ok, :error, :redirect]
end
```
However, keep in mind using a module attribute or defining the atoms in the module body, outside of a function, are not sufficient, as the module body is only executed during compilation and it is not necessarily part of the compiled module loaded at runtime.
You may alternatively use `String.to_existing_atom/1`, but keep in mind it does have pitfalls related to code loading. Read the documentation for more information.
## Long parameter list
@@ -226,7 +226,7 @@ end
While you may receive the `address` as a string from a database, web request, or a third-party, if you find yourself frequently manipulating or extracting information from the string, it is a good indicator you should convert the address into structured data:
Another example of this anti-pattern is using floating numbers to model money and currency, when [richer data structures should be preferred](https://hexdocs.pm/ex_money/).
Another example of this anti-pattern is using floating numbers to model money and currency, when [richer data structures should be preferred](https://ex-money.hexdocs.pm/).
#### Refactoring
@@ -350,7 +350,7 @@ The difference here is that the `struct/2` function behaves precisely the same f
#### Problem
The [*application environment*](https://hexdocs.pm/elixir/Application.html#module-the-application-environment) can be used to parameterize global values that can be used in an Elixir system. This mechanism can be very useful and therefore is not considered an anti-pattern by itself. However, library authors should avoid using the application environment to configure their library. The reason is exactly that the application environment is a **global** state, so there can only be a single value for each key in the environment for an application. This makes it impossible for multiple applications depending on the same library to configure the same aspect of the library in different ways.
The [*application environment*](https://elixir.hexdocs.pm/Application.html#module-the-application-environment) can be used to parameterize global values that can be used in an Elixir system. This mechanism can be very useful and therefore is not considered an anti-pattern by itself. However, library authors should avoid using the application environment to configure their library. The reason is exactly that the application environment is a **global** state, so there can only be a single value for each key in the environment for an application. This makes it impossible for multiple applications depending on the same library to configure the same aspect of the library in different ways.
#### Example
@@ -269,7 +269,7 @@ iex> heartbeats_per_minute = [99, 97, 116]
You can always force charlists to be printed in their list representation by calling the `inspect/2` function:
```elixir
iex> inspect(heartbeats_per_minute, charlists: :as_list)
iex> inspect(heartbeats_per_minute, charlists: :as_lists)
"[99, 97, 116]"
```
@@ -96,7 +96,7 @@ defmodule TestCase do
"""
defmacro test(description, do: block) do
function_name = String.to_atom("test " <> description)
function_name = String.to_unsafe_atom("test " <> description)
quote do
def unquote(function_name)(), do: unquote(block)
end
@@ -159,7 +159,7 @@ defmodule TestCase do
"""
defmacro test(description, do: block) do
function_name = String.to_atom("test " <> description)
function_name = String.to_unsafe_atom("test " <> description)
quote do
# Prepend the newly defined test to the list of tests
@tests [unquote(function_name) | @tests]
@@ -261,7 +261,7 @@ Open up `config/runtime.exs` and add this to the bottom:
nodes =
System.get_env("NODES", "")
|> String.split(",", trim: true)
|> Enum.map(&String.to_atom/1)
|> Enum.map(&String.to_unsafe_atom/1)
config :kv, :nodes, nodes
```
@@ -14,12 +14,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
New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). All security releases [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date).
@@ -49,6 +49,7 @@ Erlang/OTP versioning is independent from the versioning of Elixir. Erlang relea
Elixir version | Supported Erlang/OTP versions
:------------- | :-------------------------------
1.21 | 27 - 29
1.20 | 27 - 29
1.19 | 26 - 28
1.18 | 25 - 27
@@ -91,6 +92,7 @@ The first column is the version the feature was hard deprecated. The second colu
Version | Deprecated feature | Replaced by (available since)
:-------| :-------------------------------------------------- | :---------------------------------------------------------------
[v1.21] | `Macro.Env.fetch_alias/2` (and `fetch_macro_alias`) | `Macro.Env.expand_alias/4` (v1.17)
[v1.20] | `<<x::size(y)>>` in patterns without `^` | `<<x::size(^y)>>` (v1.15)
[v1.20] | `File.stream!(path, modes, lines_or_bytes)` | `File.stream!(path, lines_or_bytes, modes)` (v1.16)
[v1.20] | `Kernel.ParallelCompiler.async/1` | `Kernel.ParallelCompiler.pmap/2` (v1.16)
@@ -243,4 +245,5 @@ Version | Deprecated feature | Replaced by (ava
[v1.17]: https://github.com/elixir-lang/elixir/blob/v1.17/CHANGELOG.md#4-hard-deprecations
[v1.18]: https://github.com/elixir-lang/elixir/blob/v1.18/CHANGELOG.md#4-hard-deprecations
[v1.19]: https://github.com/elixir-lang/elixir/blob/v1.19/CHANGELOG.md#4-hard-deprecations
[v1.20]: https://github.com/elixir-lang/elixir/blob/main/CHANGELOG.md#4-hard-deprecations
[v1.20]: https://github.com/elixir-lang/elixir/blob/v1.20/CHANGELOG.md#4-hard-deprecations
[v1.21]: https://github.com/elixir-lang/elixir/blob/main/CHANGELOG.md#4-hard-deprecations
@@ -231,11 +231,47 @@ Inferring type signatures comes with a series of trade-offs:
* Cascading errors - when a user accidentally makes type errors or the code has conflicting assumptions, type inference may lead to less clear error messages as the type system tries to reconcile diverging type assumptions across code paths.
On the other hand, type inference offers the benefit of enabling type checking for functions and codebases without requiring the user to add type annotations. To balance these trade-offs, Elixir aims to provide "module type inference": our goal is to infer the types of functions considering the current module, Elixir's standard library and your dependencies, while calls to modules within the same project are assumed to be `dynamic()`. Once types are inferred, then the whole project is type checked considering all modules and all types (inferred or otherwise).
On the other hand, type inference offers the benefit of enabling type checking for functions and codebases without requiring the user to add type annotations. To balance these trade-offs, Elixir aims to provide type inference across dependencies: our goal is to infer the types of functions considering the current module, Elixir's standard library and your dependencies, while calls to modules within the same project are assumed to be `dynamic()`. Once types are inferred, then the whole project is type checked considering all modules and all types (inferred or otherwise).
Type inference in Elixir is best-effort: it doesn't guarantee it will find all possible type incompatibilities, only that it may find bugs where all combinations of a type _will_ fail, even in the absence of explicit type annotations. It is meant to be an efficient routine that brings developers some benefits of static typing, without requiring any effort from them and keeping the expressiveness of the language.
In the long term, Elixir developers who want static typing guarantees must explicitly add type signatures to their functions (see "Roadmap"). Any function with an explicit type signature will be typed checked against the user-provided annotations, as in other statically typed languages.
In the long term, Elixir developers who want static typing guarantees may explicitly add type signatures to their functions (see "Roadmap"). Any function with an explicit type signature will be typed checked against the user-provided annotations, as in other statically typed languages.
### False positives
Elixir's type inference generally avoids emitting false positive type violations: which are warnings emitted by the type checker when there are no runtime errors. However, in some situations, those may happen and are documented below.
#### `for`-comprehensions assume they are executed at least once
For comprehensions in Elixir assume they are executed at least once. Take this code:
```elixir
def example(x, list) do
for _i <- list do
Atom.to_string(x)
end
x + 1
end
```
`x + 1` will fail because it assumes `x` is an atom from the `Atom.to_string(x)` call, even though the function may raise no runtime error if `list` is an empty list. This is intentional, as it helps find discrepancies inside and outside comprehensions. You can address this by explicitly wrapping the comprehension in a `if list != [] do` block (or similar condition).
#### Struct update syntax must be statically proven
Elixir will warn if you use the struct update syntax and it is not statically proven that the given value does not have said struct type. For example:
```elixir
user = find_user_by_id(42)
%User{user | name: "John Doe"}
```
Even though it is guaranteed at runtime that user is always a `User` struct. If the type system cannot prove it, it will emit a typing violation. This is how stuct updates work by design. In such cases, you can address it by matching on the struct when the user variable is defined:
```elixir
%User{} = user = find_user_by_id(42)
%User{user | name: "John Doe"}
```
## Roadmap
@@ -72,6 +72,15 @@ iex> {x, x} = {1, 2}
** (MatchError) no match of right hand side value: {1, 2}
```
However, a variable cannot be defined through itself in the same pattern. This includes cases where the variable appears on both sides of a nested match operator:
```elixir
{:ok, x} = {x, :ok}
%{a: value} = %{b: value}
```
Such patterns are rejected as cyclic variable definitions. You can rewrite them by matching once and then using guards or separate comparisons for the additional constraints.
The underscore variable (`_`) has a special meaning as it can never be bound to any value. It is especially useful when you don't care about certain value in a pattern:
```iex
+1 -1
View File
@@ -146,4 +146,4 @@ See the [ORT Mix plugin documentation](https://oss-review-toolkit.org/ort/docs/p
* [CycloneDX Specification](https://cyclonedx.org/specification/overview/): learn more about the SBoM format
* [OWASP Dependency-Track](https://dependencytrack.org/): continuous SBoM analysis platform
* [mix_sbom documentation](https://hexdocs.pm/sbom): full documentation and advanced options
* [mix_sbom documentation](https://sbom.hexdocs.pm): full documentation and advanced options
+1 -1
View File
@@ -16,7 +16,7 @@
Elixir is a dynamically typed language, and as such, type specifications are never used by the compiler to optimize or modify code. Still, using type specifications is useful because:
* they provide documentation (for example, tools such as [`ExDoc`](https://hexdocs.pm/ex_doc/) show type specifications in the documentation)
* they provide documentation (for example, tools such as [`ExDoc`](https://ex-doc.hexdocs.pm/) show type specifications in the documentation)
* they're used by tools such as [Dialyzer](`:dialyzer`), that can analyze code with typespecs to find type inconsistencies and possible bugs
Type specifications (most often referred to as *typespecs*) are defined in different contexts using the following attributes:
@@ -37,7 +37,7 @@ Mixed-script identifiers are not supported for security reasons. 'аdmin' is mad
\u006E n {Latin}
Make sure all characters in the identifier resolve to a single script or a highly
restrictive script. See https://hexdocs.pm/elixir/unicode-syntax.html for more information.
restrictive script. See https://elixir.hexdocs.pm/unicode-syntax.html for more information.
```
Finally, Elixir will also warn of confusable identifiers in the same file. For example, Elixir will emit a warning if you use both variables `а` (Cyrillic) and `а` (Latin) in your code.
+1 -1
View File
@@ -85,7 +85,7 @@ defmodule CoverageRecorder do
]
defp skip_from_coverage?(file) do
mod = file |> Path.basename(".beam") |> String.to_atom()
mod = file |> Path.basename(".beam") |> String.to_unsafe_atom()
mod in @to_skip or match?({:docs_v1, _, _, _, _, %{deprecated: _}, _}, Code.fetch_docs(mod))
end
end
+5 -1
View File
@@ -42,7 +42,11 @@ latest =
version_nodes =
for version <- versions do
version_string = Version.to_string(version)
map = %{version: "v#{version_string}", url: "https://hexdocs.pm/#{app}/#{version_string}"}
map = %{
version: "v#{version_string}",
url: "https://#{String.replace(app, "_", "-")}.hexdocs.pm/#{version_string}"
}
if version_string == latest do
Map.put(map, :latest, true)
+13 -7
View File
@@ -3,7 +3,11 @@
# SPDX-FileCopyrightText: 2012 Plataformatec
# Returns config for Elixir docs (exclusively)
canonical = System.fetch_env!("CANONICAL")
canonical =
case System.fetch_env!("CANONICAL") do
"" -> System.version() <> "/"
canonical -> canonical
end
[
search: [
@@ -79,11 +83,11 @@ canonical = System.fetch_env!("CANONICAL")
"CHANGELOG.md"
],
deps: [
eex: "https://hexdocs.pm/eex/#{canonical}",
ex_unit: "https://hexdocs.pm/ex_unit/#{canonical}",
iex: "https://hexdocs.pm/iex/#{canonical}",
logger: "https://hexdocs.pm/logger/#{canonical}",
mix: "https://hexdocs.pm/mix/#{canonical}"
eex: "https://eex.hexdocs.pm/#{canonical}",
ex_unit: "https://ex-unit.hexdocs.pm/#{canonical}",
iex: "https://iex.hexdocs.pm/#{canonical}",
logger: "https://logger.hexdocs.pm/#{canonical}",
mix: "https://mix.hexdocs.pm/#{canonical}"
],
groups_for_extras: [
"Getting started": ~r"pages/getting-started/.*\.md$",
@@ -101,7 +105,9 @@ canonical = System.fetch_env!("CANONICAL")
],
skip_code_autolink_to: [
"Enumerable.List",
"Inspect.MapSet"
"Inspect.MapSet",
"Macro.Env.fetch_alias/2",
"Macro.Env.fetch_macro_alias/2"
],
formatters: ["html", "markdown", "epub"],
groups_for_modules: [
+11 -7
View File
@@ -2,7 +2,11 @@
# SPDX-FileCopyrightText: 2021 The Elixir Team
# Returns config for other apps except Elixir
canonical = System.fetch_env!("CANONICAL")
canonical =
case System.fetch_env!("CANONICAL") do
"" -> System.version() <> "/"
canonical -> canonical
end
[
search: [
@@ -21,12 +25,12 @@ canonical = System.fetch_env!("CANONICAL")
}
],
deps: [
eex: "https://hexdocs.pm/eex/#{canonical}",
elixir: "https://hexdocs.pm/elixir/#{canonical}",
ex_unit: "https://hexdocs.pm/ex_unit/#{canonical}",
iex: "https://hexdocs.pm/iex/#{canonical}",
logger: "https://hexdocs.pm/logger/#{canonical}",
mix: "https://hexdocs.pm/mix/#{canonical}"
eex: "https://eex.hexdocs.pm/#{canonical}",
elixir: "https://elixir.hexdocs.pm/#{canonical}",
ex_unit: "https://ex-unit.hexdocs.pm/#{canonical}",
iex: "https://iex.hexdocs.pm/#{canonical}",
logger: "https://logger.hexdocs.pm/#{canonical}",
mix: "https://mix.hexdocs.pm/#{canonical}"
],
formatters: ["html", "markdown", "epub"],
before_closing_body_tag: fn
+1
View File
@@ -88,6 +88,7 @@ start(_Type, _Args) ->
%% Compiler options
{debug_info, true},
{docs, true},
{erlc_options, []},
{ignore_already_consolidated, false},
{ignore_module_conflict, false},
{infer_signatures, [elixir]},
+1 -1
View File
@@ -52,7 +52,7 @@
}).
-record(elixir_erl, {
%% Can be match, guards or nil
%% Can be match, guard or nil
context=nil,
%% Extra information about the context, like pin_guard and map_key
extra=nil,
+27 -1
View File
@@ -132,7 +132,15 @@ compute_alignment(default, Size, Unit) -> compute_alignment(integer, Size, Unit)
compute_alignment(integer, default, Unit) -> compute_alignment(integer, 8, Unit);
compute_alignment(integer, Size, default) -> compute_alignment(integer, Size, 1);
compute_alignment(bitstring, Size, default) -> compute_alignment(bitstring, Size, 1);
compute_alignment(binary, _, _) -> 0;
compute_alignment(binary, default, default) -> 0;
compute_alignment(binary, default, Unit) when is_integer(Unit) ->
case Unit rem 8 of
0 -> 0;
_ -> unknown
end;
compute_alignment(binary, Size, default) -> compute_alignment(binary, Size, 8);
compute_alignment(binary, _Size, Unit) when is_integer(Unit), Unit rem 8 == 0 -> 0;
compute_alignment(binary, _, _) -> unknown;
compute_alignment(float, _, _) -> 0;
compute_alignment(utf32, _, _) -> 0;
compute_alignment(utf16, _, _) -> 0;
@@ -180,6 +188,7 @@ expand_specs(ExprType, Meta, Info, S, OriginalS, E, ExpectSize) ->
validate_size_required(Meta, ExpectSize, ExprType, MergedType, Size, ES),
SizeAndUnit = size_and_unit(Meta, ExprType, Size, Unit, ES),
Alignment = compute_alignment(MergedType, Size, Unit),
maybe_warn_binary_alignment(Meta, MergedType, Alignment, ES),
MaybeInferredSize = case {ExpectSize, MergedType, SizeAndUnit} of
{{infer, PinnedVar}, binary, []} -> [{size, Meta, [{{'.', Meta, [erlang, byte_size]}, Meta, [PinnedVar]}]}];
@@ -307,6 +316,15 @@ validate_size_required(Meta, required, default, Type, default, E) when Type == b
validate_size_required(_, _, _, _, _, _) ->
ok.
maybe_warn_binary_alignment(_Meta, binary, Alignment, _E)
when is_integer(Alignment), Alignment rem 8 == 0 ->
ok;
maybe_warn_binary_alignment(Meta, binary, Alignment, E) ->
%% TODO: Raise on Elixir v1.25
elixir_errors:file_warn(Meta, E, ?MODULE, {unaligned_binary_segment, Alignment});
maybe_warn_binary_alignment(_Meta, _Type, _Alignment, _E) ->
ok.
size_and_unit(Meta, bitstring, Size, Unit, E) when Size /= default; Unit /= default ->
function_error(Meta, E, ?MODULE, bittype_literal_bitstring),
[];
@@ -375,6 +393,14 @@ add_spec(Key, Spec) -> [{Key, [], nil} | Spec].
format_error({unaligned_binary, Expr}) ->
Message = "expected ~ts to be a binary but its number of bits is not divisible by 8",
io_lib:format(Message, ['Elixir.Macro':to_string(Expr)]);
format_error({unaligned_binary_segment, unknown}) ->
"a binary segment must have a size-unit pair that is always divisible by 8. "
"Use bitstring instead";
format_error({unaligned_binary_segment, Alignment}) ->
Message =
"a binary segment must have a size-unit pair divisible by 8, got a segment size of ~B bits. "
"Use bitstring instead",
io_lib:format(Message, [Alignment]);
format_error(unsized_binary) ->
"a binary field without size is only allowed at the end of a binary pattern, "
"at the right side of binary concatenation and never allowed in binary generators. "
+3 -3
View File
@@ -562,16 +562,16 @@ format_error({recursive, Vars, TypeExpr}) ->
Message =
case lists:map(fun({Name, Context}) -> elixir_utils:var_info(Name, Context) end, lists:sort(Vars)) of
[Var] ->
io_lib:format("the variable ~ts is defined in function of itself", [Var]);
io_lib:format("the variable ~ts depends on itself through the pattern", [Var]);
[Var1, Var2] ->
io_lib:format("the variable ~ts is defined recursively in function of ~ts", [Var1, Var2]);
io_lib:format("the variable ~ts depends on ~ts through the pattern", [Var1, Var2]);
[Head | Tail] ->
List = lists:foldl(fun(X, Acc) -> [Acc, $,, $\s, X] end, Head, Tail),
io_lib:format("the following variables form a cycle: ~ts", [List])
end,
io_lib:format(
"recursive variable definition in patterns:~n~n~ts~n~n~ts",
"cyclic variable definition in patterns:~n~n~ts~n~n~ts",
[Code, Message]
);
+17
View File
@@ -59,6 +59,9 @@ handle_call({acquire, Path}, From, Config) ->
handle_call(required, _From, Config) ->
{reply, [F || {F, true} <- maps:to_list(Config#elixir_code_server.required)], Config};
handle_call({release, Path}, _From, Config) ->
{reply, ok, release(Path, Config)};
handle_call(retrieve_compiler_module, _From, Config) ->
case Config#elixir_code_server.mod_pool of
{Used, [Mod | Unused], Counter} ->
@@ -140,6 +143,20 @@ terminate(_Reason, _Config) ->
code_change(_Old, Config, _Extra) ->
{ok, Config}.
release(Path, Config) ->
Current = Config#elixir_code_server.required,
case maps:find(Path, Current) of
{ok, []} ->
Released = maps:remove(Path, Current),
Config#elixir_code_server{required=Released};
{ok, [Next | Waiting]} ->
_ = gen_server:reply(Next, proceed),
Released = maps:put(Path, Waiting, Current),
Config#elixir_code_server{required=Released};
error ->
Config
end.
compiler_module(I) ->
list_to_atom("elixir_compiler_" ++ integer_to_list(I)).
+2 -1
View File
@@ -310,7 +310,8 @@ unpack_expanded(_Kind, _Meta, _Name, [], _Counter, Acc, Clauses) ->
expand_defaults([{'\\\\', Meta, [Expr, Default]} | Args], S, E, Acc) ->
{ExpandedDefault, SE, _} = elixir_expand:expand(Default, S, E),
expand_defaults(Args, SE, E, [{'\\\\', Meta, [Expr, ExpandedDefault]} | Acc]);
expand_defaults(Args, elixir_env:reset_read(SE, S), E,
[{'\\\\', Meta, [Expr, ExpandedDefault]} | Acc]);
expand_defaults([Arg | Args], S, E, Acc) ->
expand_defaults(Args, S, E, [Arg | Acc]);
expand_defaults([], S, _E, Acc) ->
+1 -1
View File
@@ -11,7 +11,7 @@
-define(typespecs, 'Elixir.Kernel.Typespec').
checker_version() ->
elixir_checker_v8.
elixir_checker_v9.
%% debug_info callback
+17 -3
View File
@@ -60,16 +60,30 @@ env_compiler_options() ->
erl_to_core(Forms, Opts) ->
%% TODO: Remove parse transform handling on Elixir v2.0
case [M || {parse_transform, M} <- Opts] of
[] ->
case requires_abstract_format(Opts, false) of
false ->
v3_core:module(Forms, Opts);
_ ->
beam_debug_info ->
{ok, DbgForms} = sys_coverage:beam_debug_info(Forms),
v3_core:module(DbgForms, Opts);
parse_transform ->
case compile:noenv_forms(Forms, [no_spawn_compiler_process, to_core0, return, no_auto_import | Opts]) of
{ok, _Module, Core, Warnings} -> {ok, Core, Warnings};
{error, Errors, Warnings} -> {error, Errors, Warnings}
end
end.
requires_abstract_format([{parse_transform, _} | _], _Default) ->
parse_transform;
requires_abstract_format([beam_debug_info | Tail], _Default) ->
requires_abstract_format(Tail, beam_debug_info);
requires_abstract_format([_ | Tail], Default) ->
requires_abstract_format(Tail, Default);
requires_abstract_format([], Default) ->
Default.
noenv_forms(Forms, File, Opts) when is_list(Forms), is_list(Opts), is_binary(File) ->
Source = elixir_utils:characters_to_list(File),
+50 -38
View File
@@ -14,12 +14,15 @@
translate(Meta, Args, S) ->
{Cases, [{do, Expr} | Opts]} = elixir_utils:split_last(Args),
% needs to be the original variables, excluding variables from generators
InitVars = S#elixir_erl.var_names,
case lists:keyfind(reduce, 1, Opts) of
{reduce, Reduce} -> translate_reduce(Meta, Cases, Expr, Reduce, S);
false -> translate_into(Meta, Cases, Expr, Opts, S)
{reduce, Reduce} -> translate_reduce(Meta, Cases, Expr, Reduce, InitVars, S);
false -> translate_into(Meta, Cases, Expr, Opts, InitVars, S)
end.
translate_reduce(Meta, Cases, Expr, Reduce, S) ->
translate_reduce(Meta, Cases, Expr, Reduce, InitVars, S) ->
Ann = ?ann(Meta),
{TReduce, SR} = elixir_erl_pass:translate(Reduce, Ann, S),
{TCases, SC} = translate_gen(Meta, Cases, [], SR),
@@ -30,9 +33,9 @@ translate_reduce(Meta, Cases, Expr, Reduce, S) ->
({'case', CaseAnn, _, CaseBlock}, InnerAcc) -> {'case', CaseAnn, InnerAcc, CaseBlock}
end,
build_reduce(Ann, TCases, InnerFun, TExpr, TReduce, false, SE).
build_reduce(Ann, TCases, InnerFun, TExpr, TReduce, false, InitVars, SE).
translate_into(Meta, Cases, Expr, Opts, S) ->
translate_into(Meta, Cases, Expr, Opts, InitVars, S) ->
Ann = ?ann(Meta),
{TInto, SI} =
@@ -47,8 +50,8 @@ translate_into(Meta, Cases, Expr, Opts, S) ->
{TExpr, SE} = elixir_erl_pass:translate(wrap_expr_if_unused(Expr, TInto), Ann, SC),
case inline_or_into(TInto) of
inline -> build_inline(Ann, TCases, TExpr, TInto, TUniq, SE);
into -> build_into(Ann, TCases, TExpr, TInto, TUniq, SE)
inline -> build_inline(Ann, TCases, TExpr, TInto, TUniq, InitVars, SE);
into -> build_into(Ann, TCases, TExpr, TInto, TUniq, InitVars, SE)
end.
%% In case we have no return, we wrap the expression
@@ -115,29 +118,29 @@ collect_filters([H | T], Acc) ->
collect_filters([], Acc) ->
{Acc, []}.
build_inline(Ann, Clauses, Expr, Into, Uniq, S) ->
build_inline(Ann, Clauses, Expr, Into, Uniq, InitVars, S) ->
case not Uniq and lists:all(fun(Clause) -> element(1, Clause) == bin end, Clauses) of
true -> {build_comprehension(Ann, Clauses, Expr, Into), S};
false -> build_inline_each(Ann, Clauses, Expr, Into, Uniq, S)
false -> build_inline_each(Ann, Clauses, Expr, Into, Uniq, InitVars, S)
end.
build_inline_each(Ann, Clauses, Expr, false, Uniq, S) ->
build_inline_each(Ann, Clauses, Expr, false, Uniq, InitVars, S) ->
InnerFun = fun(InnerExpr, _InnerAcc) -> InnerExpr end,
build_reduce(Ann, Clauses, InnerFun, Expr, {nil, Ann}, Uniq, S);
build_inline_each(Ann, [{enum, _, Left = {var, _, _}, Right, [] = _Filters}], Expr, {nil, _} = _Into, false, S) ->
build_reduce(Ann, Clauses, InnerFun, Expr, {nil, Ann}, Uniq, InitVars, S);
build_inline_each(Ann, [{enum, _, Left = {var, _, _}, Right, [] = _Filters}], Expr, {nil, _} = _Into, false, _InitVars, S) ->
Clauses = [{clause, Ann, [Left], [], [Expr]}],
Args = [Right, {'fun', Ann, {clauses, Clauses}}],
{?remote(Ann, 'Elixir.Enum', map, Args), S};
build_inline_each(Ann, [{enum, _, Left = {var, _, _}, Right, [] = _Filters}], Expr, {map, _, []} = _Into, false, S) ->
build_inline_each(Ann, [{enum, _, Left = {var, _, _}, Right, [] = _Filters}], Expr, {map, _, []} = _Into, false, _InitVars, S) ->
Clauses = [{clause, Ann, [Left], [], [Expr]}],
Args = [Right, {'fun', Ann, {clauses, Clauses}}],
List = ?remote(Ann, 'Elixir.Enum', map, Args),
{?remote(Ann, maps, from_list, [List]), S};
build_inline_each(Ann, Clauses, Expr, {nil, _} = Into, Uniq, S) ->
build_inline_each(Ann, Clauses, Expr, {nil, _} = Into, Uniq, InitVars, S) ->
InnerFun = fun(InnerExpr, InnerAcc) -> {cons, Ann, InnerExpr, InnerAcc} end,
{ReduceExpr, SR} = build_reduce(Ann, Clauses, InnerFun, Expr, Into, Uniq, S),
{ReduceExpr, SR} = build_reduce(Ann, Clauses, InnerFun, Expr, Into, Uniq, InitVars, S),
{?remote(Ann, lists, reverse, [ReduceExpr]), SR};
build_inline_each(Ann, Clauses, Expr, {bin, _, []}, Uniq, S) ->
build_inline_each(Ann, Clauses, Expr, {bin, _, []}, Uniq, InitVars, S) ->
{InnerValue, SV} = build_var(Ann, S),
Generated = erl_anno:set_generated(true, Ann),
@@ -155,17 +158,17 @@ build_inline_each(Ann, Clauses, Expr, {bin, _, []}, Uniq, S) ->
]}
end,
{ReduceExpr, SR} = build_reduce(Ann, Clauses, InnerFun, Expr, {nil, Ann}, Uniq, SV),
{ReduceExpr, SR} = build_reduce(Ann, Clauses, InnerFun, Expr, {nil, Ann}, Uniq, InitVars, SV),
{?remote(Ann, erlang, list_to_bitstring, [ReduceExpr]), SR}.
build_into(Ann, Clauses, Expr, {map, _, []}, Uniq, S) ->
{ReduceExpr, SR} = build_inline_each(Ann, Clauses, Expr, {nil, Ann}, Uniq, S),
build_into(Ann, Clauses, Expr, {map, _, []}, Uniq, InitVars, S) ->
{ReduceExpr, SR} = build_inline_each(Ann, Clauses, Expr, {nil, Ann}, Uniq, InitVars, S),
{?remote(Ann, maps, from_list, [ReduceExpr]), SR};
build_into(Ann, Clauses, Expr, ?empty_map_set_pattern = _Into, Uniq, S) ->
build_into(Ann, Clauses, Expr, ?empty_map_set_pattern = _Into, Uniq, InitVars, S) ->
InnerFun = fun(InnerExpr, InnerAcc) -> {cons, Ann, InnerExpr, InnerAcc} end,
{ReduceExpr, SR} = build_reduce(Ann, Clauses, InnerFun, Expr, {nil, Ann}, Uniq, S),
{ReduceExpr, SR} = build_reduce(Ann, Clauses, InnerFun, Expr, {nil, Ann}, Uniq, InitVars, S),
{?remote(Ann, 'Elixir.MapSet', new, [ReduceExpr]), SR};
build_into(Ann, Clauses, Expr, Into, Uniq, S) ->
build_into(Ann, Clauses, Expr, Into, Uniq, InitVars, S) ->
{Fun, SF} = build_var(Ann, S),
{Acc, SA} = build_var(Ann, SF),
{Kind, SK} = build_var(Ann, SA),
@@ -182,7 +185,7 @@ build_into(Ann, Clauses, Expr, Into, Uniq, S) ->
?remote(Ann, 'Elixir.Collectable', into, [Into])
},
{IntoReduceExpr, SN} = build_reduce(Ann, Clauses, InnerFun, Expr, Acc, Uniq, SD),
{IntoReduceExpr, SN} = build_reduce(Ann, Clauses, InnerFun, Expr, Acc, Uniq, InitVars, SD),
TryExpr =
{'try', Ann,
@@ -205,10 +208,10 @@ stacktrace_clause(Ann, Fun, Acc, Kind, Reason, Stack) ->
%% Helpers
build_reduce(Ann, Clauses, InnerFun, Expr, Into, false, S) ->
build_reduce(Ann, Clauses, InnerFun, Expr, Into, false, InitVars, S) ->
{Acc, SA} = build_var(Ann, S),
{build_reduce_each(Clauses, InnerFun(Expr, Acc), Into, Acc, SA), SA};
build_reduce(Ann, Clauses, InnerFun, Expr, Into, true, S) ->
{build_reduce_each(Clauses, InnerFun(Expr, Acc), Into, Acc, InitVars, SA), SA};
build_reduce(Ann, Clauses, InnerFun, Expr, Into, true, InitVars, S) ->
%% Those variables are used only inside the anonymous function
%% so we don't need to worry about returning the scope.
{Acc, SA} = build_var(Ann, S),
@@ -229,12 +232,12 @@ build_reduce(Ann, Clauses, InnerFun, Expr, Into, true, S) ->
]}
]},
EnumReduceCall = build_reduce_each(Clauses, InnerExpr, NewInto, Acc, SU),
EnumReduceCall = build_reduce_each(Clauses, InnerExpr, NewInto, Acc, InitVars, SU),
{?remote(Ann, erlang, element, [{integer, Ann, 1}, EnumReduceCall]), SU}.
build_reduce_each([{enum, Meta, Left, Right, Filters} | T], Expr, Arg, Acc, S) ->
build_reduce_each([{enum, Meta, Left, Right, Filters} | T], Expr, Arg, Acc, InitVars, S) ->
Ann = ?ann(Meta),
True = build_reduce_each(T, Expr, Acc, Acc, S),
True = build_reduce_each(T, Expr, Acc, Acc, InitVars, S),
False = Acc,
Generated = erl_anno:set_generated(true, Ann),
@@ -255,13 +258,13 @@ build_reduce_each([{enum, Meta, Left, Right, Filters} | T], Expr, Arg, Acc, S) -
Args = [Right, Arg, {'fun', Ann, {clauses, Clauses1}}],
?remote(Ann, 'Elixir.Enum', reduce, Args);
build_reduce_each([{bin, Meta, Left, Right, Filters} | T], Expr, Arg, Acc, S) ->
build_reduce_each([{bin, Meta, Left, Right, Filters} | T], Expr, Arg, Acc, InitVars, S) ->
Ann = ?ann(Meta),
Generated = erl_anno:set_generated(true, Ann),
{Tail, ST} = build_var(Ann, S),
{Fun, SF} = build_var(Ann, ST),
True = build_reduce_each(T, Expr, Acc, Acc, SF),
True = build_reduce_each(T, Expr, Acc, Acc, InitVars, SF),
False = Acc,
{bin, _, Elements} = Left,
TailElement = {bin_element, Ann, Tail, default, [bitstring]},
@@ -275,7 +278,7 @@ build_reduce_each([{bin, Meta, Left, Right, Filters} | T], Expr, Arg, Acc, S) ->
[?remote(Ann, erlang, error, [pair(Ann, badarg, Tail)])]}],
NoVarClauses =
case no_var(Generated, Elements) of
case no_unbound_var(Generated, Elements, InitVars) of
error ->
Clauses;
@@ -294,7 +297,7 @@ build_reduce_each([{bin, Meta, Left, Right, Filters} | T], Expr, Arg, Acc, S) ->
{named_fun, Ann, element(3, Fun), VarClauses},
[Right, Arg]};
build_reduce_each([], Expr, _Arg, _Acc, _S) ->
build_reduce_each([], Expr, _Arg, _Acc, _InitVars, _S) ->
Expr.
is_var({var, _, _}) -> true;
@@ -307,9 +310,10 @@ build_var(Ann, S) ->
{Name, ST} = elixir_erl_var:build('_', S),
{{var, Ann, Name}, ST}.
no_var(ParentAnn, Elements) ->
no_unbound_var(ParentAnn, Elements, InitVars) ->
Vars = #{V => K || K := V <- InitVars},
try
[{bin_element, Ann, NoVarExpr, no_var_size(Size), Types} ||
[{bin_element, Ann, NoVarExpr, no_unbound_var_size(Size, Vars), Types} ||
{bin_element, Ann, Expr, Size, Types} <- Elements,
NoVarExpr <- no_var_expr(ParentAnn, Expr)]
catch
@@ -319,9 +323,17 @@ no_var(ParentAnn, Elements) ->
no_var_expr(Ann, {string, _, String}) -> [{var, Ann, '_'} || _ <- String];
no_var_expr(Ann, _) -> [{var, Ann, '_'}].
no_var_size(default) -> default;
no_var_size(Size) when is_integer(Size) -> Size;
no_var_size(_) -> throw(unbound_size).
no_unbound_var_size(Size, Vars) ->
valid_var_size(Size, Vars) orelse throw(unbound_size),
Size.
valid_var_size({var, _, Var}, Vars) when is_map_key(Var, Vars) -> true;
valid_var_size(default, _Vars) -> true;
valid_var_size({integer, _, _}, _Vars) -> true;
valid_var_size(Size, _Vars) when is_integer(Size) -> true;
valid_var_size({op, _Ann, _Op, Left, Right}, Vars) ->
valid_var_size(Left, Vars) andalso valid_var_size(Right, Vars);
valid_var_size(_Size, _vars) -> false.
build_comprehension(Ann, Clauses, Expr, Into) ->
{comprehension_kind(Into), Ann, Expr, comprehension_clause(Clauses)}.
+43 -10
View File
@@ -576,6 +576,10 @@ translate_remote('Elixir.String.Chars', to_string, Meta, [Arg], S) ->
{clause, Generated, [Var], [[Guard]], [Fast]},
{clause, Generated, [Var], [], [Slow]}
]}, VS};
translate_remote(lists, member, Meta, [Expr, []], S) ->
Ann = ?ann(Meta),
{TExpr, S1} = translate(Expr, Ann, S),
{{block, Ann, [{match, Ann, {var, Ann, '_'}, TExpr}, {atom, Ann, false}]}, S1};
translate_remote(lists, member, Meta, [Expr, [Head | Tail] = List], S) ->
Ann = ?ann(Meta),
@@ -630,10 +634,35 @@ translate_remote(maps, merge, Meta, [Map1, Map2], S) ->
{[TMap1, TMap2], TS} ->
{{call, Ann, {remote, Ann, {atom, Ann, maps}, {atom, Ann, merge}}, [TMap1, TMap2]}, TS}
end;
translate_remote(Mod, to_existing_atom, Meta, [String, List], S) when Mod == 'Elixir.String'; Mod == 'Elixir.List' ->
Ann = ?ann(Meta),
{[TString, TList], TS} = translate_args([String, List], Ann, S),
case is_list(List) andalso lists:all(fun is_atom/1, List) of
true ->
Generated = erl_anno:set_generated(true, Ann),
LastClause = {clause, Generated,
[{var, Generated, '_'}],
[],
[{call, Ann, {remote, Ann, {atom, Ann, Mod}, {atom, Ann, '__to_existing_atom__'}}, [TString, TList]}]},
CastAtom = case Mod of
'Elixir.String' -> fun atom_to_binary/1;
'Elixir.List' -> fun atom_to_list/1
end,
Clauses = [
{clause, Generated,
[elixir_erl:elixir_to_erl(CastAtom(Atom), Ann)],
[],
[{atom, Ann, Atom}]}
|| Atom <- List] ++ [LastClause],
{{'case', Generated, TString, Clauses}, TS};
false ->
{{call, Ann, {remote, Ann, {atom, Ann, Mod}, {atom, Ann, to_existing_atom}}, [TString, TList]}, TS}
end;
translate_remote(Left, Right, Meta, Args, S) ->
Ann = ?ann(Meta),
case rewrite_strategy(Left, Right, Args) of
case rewrite_strategy(Left, Right, Args, S) of
guard_op ->
{TArgs, SA} = translate_args(Args, Ann, S),
%% Rewrite Erlang function calls as operators so they
@@ -671,13 +700,9 @@ optimize_list_membership([], _Count) ->
optimize_list_membership(_, _Count) ->
false.
rewrite_strategy(erlang, Right, Args) ->
Arity = length(Args),
case elixir_utils:guard_op(Right, Arity) of
true -> guard_op;
false -> none
end;
rewrite_strategy(Left, shift, [Struct, Opts | RestArgs]) when
rewrite_strategy(erlang, Right, Args, S) ->
guard_op_strategy(Right, length(Args), S);
rewrite_strategy(Left, shift, [Struct, Opts | RestArgs], _S) when
Left == 'Elixir.Date';
Left == 'Elixir.DateTime';
Left == 'Elixir.NaiveDateTime';
@@ -694,7 +719,7 @@ rewrite_strategy(Left, shift, [Struct, Opts | RestArgs]) when
false ->
none
end;
rewrite_strategy(Left, Right, Args) ->
rewrite_strategy(Left, Right, Args, _S) ->
case inline_pure_function(Left, Right) andalso basic_type_arg(Args) of
true ->
try
@@ -707,13 +732,21 @@ rewrite_strategy(Left, Right, Args) ->
reorder_strategy(Left, Right, length(Args))
end.
guard_op_strategy('andalso', 2, #elixir_erl{context=guard}) -> guard_op;
guard_op_strategy('orelse', 2, #elixir_erl{context=guard}) -> guard_op;
guard_op_strategy(Op, Arity, _S) ->
case elixir_utils:guard_op(Op, Arity) of
true -> guard_op;
false -> none
end.
-define(
reorder(ExMod, ExFun, ExArity, ExArgs, ErlMod, ErlFun, ErlArgs),
reorder_strategy(ExMod, ExFun, ExArity) -> {reorder, ErlMod, ErlFun, fun(Ann, ExArgs) -> _ = Ann, ErlArgs end}
).
?reorder('Elixir.Kernel', elem, 2, [Tuple, Index], erlang, element, [increment(Ann, Index), Tuple]);
?reorder('Elixir.Kernel', put_elem, 2, [Tuple, Index, Term], erlang, element, [increment(Ann, Index), Tuple, Term]);
?reorder('Elixir.Kernel', put_elem, 3, [Tuple, Index, Term], erlang, setelement, [increment(Ann, Index), Tuple, Term]);
?reorder('Elixir.Kernel', is_map_key, 2, [Map, Key], erlang, is_map_key, [Key, Map]);
?reorder('Elixir.Map', delete, 2, [Map, Key], maps, remove, [Key, Map]);
?reorder('Elixir.Map', fetch, 2, [Map, Key], maps, find, [Key, Map]);
+65 -43
View File
@@ -266,6 +266,9 @@ expand({quote, Meta, [Opts, Do]}, S, E) when is_list(Do) ->
Unquote = proplists:get_value(unquote, EOpts, DefaultUnquote),
Generated = proplists:get_value(generated, EOpts, false),
(map_get(context, E) /= nil) andalso Unquote andalso elixir_quote:has_unquotes(Exprs) andalso
file_error(Meta, E, ?MODULE, quote_in_pattern_with_unquote),
{Q, QContext, QPrelude} = elixir_quote:build(Meta, Line, File, Context, Unquote, Generated, ET),
{EPrelude, SP, EP} = expand(QPrelude, ST, ET),
{EContext, SC, EC} = expand(QContext, SP, EP),
@@ -287,9 +290,15 @@ expand({quote, Meta, [Opts, Do]}, S, E) when is_list(Do) ->
_ -> {'{}', [], ['__block__', [], EBinding ++ [EQuoted]]}
end,
case EPrelude of
[] -> {EBindingQuoted, ES, EQ};
_ -> {{'__block__', [], EPrelude ++ [EBindingQuoted]}, ES, EQ}
EBlock =
case EPrelude of
[] -> EBindingQuoted;
_ -> {'__block__', [], EPrelude ++ [EBindingQuoted]}
end,
case ?key(E, context) of
nil -> {{{'.', Meta, [elixir_quote, validate_quote]}, Meta, [EBlock]}, ES, EQ};
_ -> {EBlock, ES, EQ}
end;
expand({quote, Meta, [_, _]}, _S, E) ->
@@ -445,32 +454,49 @@ expand({Name, Meta, Kind}, S, E) when is_atom(Name), is_atom(Kind) ->
#{Pair := CurrentVersion} ->
case Prematch of
{Pre, _Cycle, {bitsize, Original}} ->
if
map_get(Pair, Pre) /= CurrentVersion ->
case {Original, Pre} of
{#{Pair := OriginalVersion}, _} when OriginalVersion /= CurrentVersion ->
{ok, CurrentVersion};
is_map_key(Pair, Pre) ->
{#{Pair := CurrentVersion}, #{Pair := CurrentVersion}} ->
%% TODO: Remove me on Elixir 2.0
elixir_errors:file_warn(Meta, E, ?MODULE, {unpinned_bitsize_var, Name, Kind}),
{ok, CurrentVersion};
not is_map_key(Pair, Original) ->
{ok, CurrentVersion};
{#{Pair := CurrentVersion}, #{Pair := _}} ->
{raise, {unpinned_bitsize_var, Name, Kind}};
true ->
raise
{#{Pair := CurrentVersion}, _} ->
{raise, {undefined_var, Name, Kind}};
{_, _} ->
{ok, CurrentVersion}
end;
_ ->
{ok, CurrentVersion}
end;
_ when S#elixir_ex.prematch =:= pin ->
{raise, {undefined_var_pin, Name, Kind}};
_ ->
case E of
#{context := guard} -> raise;
#{} when S#elixir_ex.prematch =:= pin -> pin;
%% TODO: Remove fallback on on_undefined_variable
_ -> elixir_config:get(on_undefined_variable)
case lists:keyfind(if_undefined, 1, Meta) of
{if_undefined, apply} ->
apply;
{if_undefined, raise} ->
{raise, {undefined_var, Name, Kind}};
_ when ?key(E, context) =:= guard ->
{raise, {undefined_var, Name, Kind}};
_ ->
%% TODO: Remove fallback as it should always raise
case elixir_config:get(on_undefined_variable) of
raise -> {raise, {undefined_var, Name, Kind}};
Other -> Other
end
end
end,
@@ -480,30 +506,18 @@ expand({Name, Meta, Kind}, S, E) when is_atom(Name), is_atom(Kind) ->
Var = {Name, [{version, PairVersion} | Meta], Kind},
{Var, S#elixir_ex{unused=var_used(Pair, Meta, PairVersion, Unused)}, E};
Error ->
case lists:keyfind(if_undefined, 1, Meta) of
{if_undefined, apply} ->
expand({Name, Meta, []}, S, E);
{raise, Reason} ->
SpanMeta = elixir_env:calculate_span(Meta, Name),
function_error(SpanMeta, E, ?MODULE, Reason),
{{Name, SpanMeta, Kind}, S#elixir_ex{tainted_function=true}, E};
%% TODO: Remove this clause on v2.0 as we will raise by default
{if_undefined, raise} ->
function_error(Meta, E, ?MODULE, {undefined_var, Name, Kind}),
{{Name, Meta, Kind}, S#elixir_ex{tainted_function=true}, E};
apply ->
expand({Name, Meta, []}, S, E);
%% TODO: Remove this clause on v2.0 as we will no longer support warn
_ when Error == warn ->
elixir_errors:file_warn(Meta, E, ?MODULE, {undefined_var_to_call, Name}),
expand({Name, [{if_undefined, warn} | Meta], []}, S, E);
_ when Error == pin ->
function_error(Meta, E, ?MODULE, {undefined_var_pin, Name, Kind}),
{{Name, Meta, Kind}, S#elixir_ex{tainted_function=true}, E};
_ when Error == raise ->
SpanMeta = elixir_env:calculate_span(Meta, Name),
function_error(SpanMeta, E, ?MODULE, {undefined_var, Name, Kind}),
{{Name, SpanMeta, Kind}, S#elixir_ex{tainted_function=true}, E}
end
%% TODO: Remove this clause on v2.0 as we will no longer support warn
warn ->
elixir_errors:file_warn(Meta, E, ?MODULE, {undefined_var_to_call, Name}),
expand({Name, [{if_undefined, warn} | Meta], []}, S, E)
end;
%% Local calls
@@ -964,9 +978,13 @@ expand_remote(Receiver, DotMeta, Right, Meta, Args, S, SL, #{context := Context}
{EArgs, {SA, _}, EA} = mapfold(fun expand_arg/3, {SL, S}, E, Args),
SA#elixir_ex.tainted_function andalso is_atom(Receiver) andalso
(not is_loaded_and_exported(Receiver, Right, Args)) andalso
is_loaded_and_not_exported(Receiver, Right, Args) andalso
elixir_errors:file_warn(Meta, E, ?MODULE, {undefined_function, Receiver, Right, length(Args)}),
%% TODO: Raise on Elixir v2.0
(Receiver =:= erlang) andalso ((Right =:= 'orelse') orelse (Right =:= 'andalso')) andalso
elixir_errors:file_warn(Meta, E, ?MODULE, {invalid_guard_operator, Right}),
Rewritten = elixir_rewrite:rewrite(Receiver, DotMeta, Right, AttachedMeta, EArgs),
{Rewritten, elixir_env:close_write(SA, S), EA};
@@ -987,9 +1005,9 @@ expand_remote(Receiver, DotMeta, Right, Meta, Args, _, _, E) ->
Call = {{'.', DotMeta, [Receiver, Right]}, Meta, Args},
file_error(Meta, E, ?MODULE, {invalid_call, Call}).
is_loaded_and_exported(Receiver, Fun, Args) ->
is_loaded_and_not_exported(Receiver, Fun, Args) ->
(code:ensure_loaded(Receiver) =:= {module, Receiver}) andalso
erlang:function_exported(Receiver, Fun, length(Args)).
not erlang:function_exported(Receiver, Fun, length(Args)).
attach_runtime_module(Receiver, Meta, S, _E) ->
case lists:member(Receiver, S#elixir_ex.runtime_modules) of
@@ -1210,6 +1228,8 @@ format_error({expected_compile_time_module, Kind, GivenTerm}) ->
format_error({unquote_outside_quote, Unquote}) ->
%% Unquote can be "unquote" or "unquote_splicing".
io_lib:format("~p called outside quote", [Unquote]);
format_error(quote_in_pattern_with_unquote) ->
"unquote is not allowed when quote is used inside a pattern or guard";
format_error({invalid_bind_quoted_for_quote, BQ}) ->
io_lib:format("invalid :bind_quoted for quote, expected a keyword list of variable names, got: ~ts",
['Elixir.Macro':to_string(BQ)]);
@@ -1242,12 +1262,12 @@ format_error({invalid_expr_in_scope, Scope, Kind}) ->
format_error({invalid_expr_in_guard, Kind}) ->
Message =
"invalid expression in guards, ~ts is not allowed in guards. To learn more about "
"guards, visit: https://hexdocs.pm/elixir/patterns-and-guards.html#guards",
"guards, visit: https://elixir.hexdocs.pm/patterns-and-guards.html#guards",
io_lib:format(Message, [Kind]);
format_error({invalid_expr_in_bitsize, Kind}) ->
Message =
"~ts is not allowed inside a bitstring size specifier. The size specifier in matches works like guards. "
"To learn more about guards, visit: https://hexdocs.pm/elixir/patterns-and-guards.html#guards",
"To learn more about guards, visit: https://elixir.hexdocs.pm/patterns-and-guards.html#guards",
io_lib:format(Message, [Kind]);
format_error({invalid_alias, Expr}) ->
Message =
@@ -1299,6 +1319,8 @@ format_error({options_are_not_keyword, Kind, Opts}) ->
[Kind, 'Elixir.Macro':to_string(Opts)]);
format_error({undefined_function, Name, Args}) ->
io_lib:format("undefined function ~ts/~B (there is no such import)", [Name, length(Args)]);
format_error({invalid_guard_operator, Name}) ->
io_lib:format(":erlang.~ts/2 must only be used inside guards", [Name]);
format_error({unpinned_bitsize_var, Name, Kind}) ->
io_lib:format("the variable ~ts is accessed inside size(...) of a bitstring "
"but it was defined outside of the match. You must precede it with the "
@@ -1340,4 +1362,4 @@ format_error({parens_map_lookup, Map, Field, Context}) ->
format_error({super_in_genserver, {Name, Arity}}) ->
io_lib:format("calling super for GenServer callback ~ts/~B is deprecated", [Name, Arity]);
format_error('__cursor__') ->
"reserved special form __cursor__ cannot be expanded, it is used exclusively to annotate ASTs".
"reserved special form __cursor__ cannot be expanded, it is used exclusively to annotate ASTs".
+1 -1
View File
@@ -205,7 +205,7 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) ->
'Elixir.Module.Types':infer(Module, File, Attributes, AllDefinitions, UsedPrivate, E, CheckerInfo)
end,
RawCompileOpts = bag_lookup_element(DataBag, {accumulate, compile}, 2),
RawCompileOpts = bag_lookup_element(DataBag, {accumulate, compile}, 2) ++ elixir_config:get(erlc_options),
CompileOpts = validate_compile_opts(RawCompileOpts, AllDefinitions, Unreachable, Line, E),
Impls = bag_lookup_element(DataBag, impls, 2),
+23 -1
View File
@@ -355,6 +355,12 @@ stab_expr -> empty_paren stab_op_eol_and_expr :
build_op_with_meta([], '$2', parens_meta('$1')).
stab_expr -> empty_paren when_op expr stab_op_eol_and_expr :
build_op_with_meta([{'when', meta_from_token('$2'), ['$3']}], '$4', parens_meta('$1')).
stab_expr -> at_op_eol no_parens_one_expr stab_op_eol_and_expr :
build_at_stab('$1', '$2', '$3').
stab_expr -> at_op_eol no_parens_one_ambig_expr stab_op_eol_and_expr :
build_at_stab('$1', '$2', '$3').
stab_expr -> at_op_eol no_parens_many_expr stab_op_eol_and_expr :
build_at_stab('$1', '$2', '$3').
stab_expr -> call_args_no_parens_all stab_op_eol_and_expr :
build_op(unwrap_when(unwrap_splice('$1')), '$2').
stab_expr -> stab_parens_many stab_op_eol_and_expr :
@@ -772,7 +778,15 @@ build_unary_op({_Kind, {Line, Column, _}, '//'}, Expr) ->
{'/', [{line, Line} | Outer], [{'/', [{line, Line} | Inner], nil}, Expr]};
build_unary_op({_Kind, Location, Op}, Expr) ->
{Op, meta_from_location(Location), [Expr]}.
{Op, meta_from_location(Location), [normalize_unary_op(Op, Expr)]}.
build_at_stab(At, {Call, Meta, Args}, {Op, Right}) ->
build_unary_op(At, {Call, Meta, [build_op(Args, Op, Right)]}).
normalize_unary_op('@', {Call, Meta, [[{'->', _, [_, _]} = Stab]]}) when is_atom(Call), is_list(Meta) ->
{Call, Meta, [Stab]};
normalize_unary_op(_, Expr) ->
Expr.
build_nullary_op({_Kind, Location, Op}) ->
{Op, meta_from_location(Location), []}.
@@ -1110,6 +1124,9 @@ check_stab([{'->', _, [_, _]}], _) -> stab;
check_stab([], none) -> block;
check_stab([_], none) -> block;
check_stab([_], Meta) -> error_invalid_stab(Meta);
check_stab([{'->', Meta, [[{'@', _, [{Call, CallMeta, nil}]}], _]} | T], _)
when is_atom(Call) andalso is_list(CallMeta) ->
check_stab(T, {ambiguous_at_stab, Meta});
check_stab([{'->', Meta, [_, _]} | T], _) -> check_stab(T, Meta);
check_stab([_ | T], MaybeMeta) -> check_stab(T, MaybeMeta).
@@ -1188,6 +1205,11 @@ return_error({Line, Column, _}, ErrorMessage, ErrorToken) ->
return_error_with_meta(Meta, ErrorMessage, ErrorToken) ->
return_error(Meta, [ErrorMessage, ErrorToken]).
error_invalid_stab({ambiguous_at_stab, MetaStab}) ->
return_error_with_meta(MetaStab,
"ambiguous use of @ with ->. If you want a nullary signature, write @sig () -> bar or @sig (-> bar). "
"Syntax error before: ", "'->'");
error_invalid_stab(MetaStab) ->
return_error_with_meta(MetaStab,
"unexpected operator ->. If you want to define multiple clauses, the first expression must use ->. "
+43 -32
View File
@@ -7,7 +7,9 @@
-feature(maybe_expr, enable).
-export([escape/3, linify/3, linify_with_context_counter/3, build/7, quote/2, has_unquotes/1, fun_to_quoted/1]).
-export([dot/5, tail_list/3, list/2, validate_runtime/2, shallow_validate_ast/1]). %% Quote callbacks
%% Quote callbacks (appear in code, must be handled by the type system)
-export([dot/5, tail_list/3, list/2, unquote/1, validate_quote/1, validate_runtime/2, shallow_validate_ast/1]).
-include("elixir.hrl").
-define(defs(Kind), Kind == def; Kind == defp; Kind == defmacro; Kind == defmacrop; Kind == '@').
@@ -23,7 +25,7 @@
imports_hygiene=nil,
unquote=true,
generated=false,
shallow_validate=false
validate=false
}).
%% fun_to_quoted
@@ -273,11 +275,14 @@ build(Meta, Line, File, Context, Unquote, Generated, E) ->
unquote=Unquote,
context=VContext,
generated=Generated,
shallow_validate=true
validate=true
},
{Q, VContext, Acc3}.
validate_quote(Expr) ->
Expr.
validate_compile(_Meta, line, Value, Acc) when is_boolean(Value) ->
{Value, Acc};
validate_compile(_Meta, file, nil, Acc) ->
@@ -312,27 +317,6 @@ is_valid(context, Context) -> is_atom(Context) andalso (Context /= nil);
is_valid(generated, Generated) -> is_boolean(Generated);
is_valid(unquote, Unquote) -> is_boolean(Unquote).
shallow_validate_ast(Expr) ->
case shallow_valid_ast(Expr) of
true -> Expr;
false -> argument_error(
<<"tried to unquote invalid AST: ", ('Elixir.Kernel':inspect(Expr))/binary,
"\nDid you forget to escape term using Macro.escape/1?">>)
end.
shallow_valid_ast(Expr) when is_list(Expr) -> valid_ast_list(Expr);
shallow_valid_ast(Expr) -> valid_ast_elem(Expr).
valid_ast_list([]) -> true;
valid_ast_list([Head | Tail]) -> valid_ast_elem(Head) andalso valid_ast_list(Tail);
valid_ast_list(_Improper) -> false.
valid_ast_elem(Expr) when is_list(Expr); is_atom(Expr); is_binary(Expr); is_number(Expr); is_pid(Expr); is_function(Expr) -> true;
valid_ast_elem({Left, Right}) -> valid_ast_elem(Left) andalso valid_ast_elem(Right);
valid_ast_elem({Atom, Meta, Args}) when is_atom(Atom), is_list(Meta), is_atom(Args) orelse is_list(Args) -> true;
valid_ast_elem({Call, Meta, Args}) when is_list(Meta), is_list(Args) -> shallow_valid_ast(Call);
valid_ast_elem(_Term) -> false.
quote({unquote_splicing, _, [_]}, #elixir_quote{unquote=true}) ->
argument_error(<<"unquote_splicing only works inside arguments and block contexts, "
"wrap it in parens if you want it to work with one-liners">>);
@@ -362,9 +346,9 @@ do_quote({quote, Meta, [Opts, Arg]}, Q) when is_list(Meta) ->
{'{}', [], [quote, meta(NewMeta, Q), [TOpts, TArg]]};
do_quote({unquote, Meta, [Expr]}, #elixir_quote{unquote=true, shallow_validate=Validate}) when is_list(Meta) ->
do_quote({unquote, Meta, [Expr]}, #elixir_quote{unquote=true, validate=Validate}) when is_list(Meta) ->
case Validate of
true -> {{'.', Meta, [?MODULE, shallow_validate_ast]}, Meta, [Expr]};
true -> {{'.', Meta, [?MODULE, unquote]}, Meta, [Expr]};
false -> Expr
end;
@@ -490,7 +474,7 @@ collect_trace_import_quoted([], _Mod, Acc, Arities) ->
do_quote_call(Left, Meta, Expr, Args, Q) ->
All = [Left, {unquote, Meta, [Expr]}, Args, Q#elixir_quote.context],
TAll = [do_quote(X, Q) || X <- All],
{{'.', Meta, [elixir_quote, dot]}, Meta, [meta(Meta, Q) | TAll]}.
{{'.', Meta, [?MODULE, dot]}, Meta, [meta(Meta, Q) | TAll]}.
do_quote_tuple({Left, Meta, Right}, Q) ->
do_quote_tuple(Left, Meta, Right, Q).
@@ -533,12 +517,14 @@ do_list_concat([], Right) -> Right;
do_list_concat(Left, Right) -> {{'.', [], [erlang, '++']}, [], [Left, Right]}.
do_runtime_list(Meta, Fun, Args) ->
{{'.', Meta, [elixir_quote, Fun]}, Meta, Args}.
{{'.', Meta, [?MODULE, Fun]}, Meta, Args}.
%% Callbacks
%% Unquote validation callbacks
%%
%% They perform shallow runtime validation for performance
%% reasons but will be type checked in the future for full
%% validation.
%% Some expressions cannot be unquoted at compilation time.
%% This function is responsible for doing runtime unquoting.
dot(Meta, Left, Right, Args, Context) ->
annotate(dot(Meta, Left, Right, Args), Context).
@@ -589,12 +575,37 @@ tail_list(Left, Right, Tail) when is_list(Left) ->
end.
validate_list(List) ->
case valid_ast_list(List) of
case shallow_valid_list(List) of
true -> ok;
false -> argument_error(<<"expected a list with quoted expressions in unquote_splicing/1, got: ",
('Elixir.Kernel':inspect(List))/binary>>)
end.
unquote(Expr) ->
case shallow_valid_ast(Expr) of
true -> Expr;
false -> argument_error(
<<"tried to unquote invalid AST: ", ('Elixir.Kernel':inspect(Expr))/binary,
"\nDid you forget to escape term using Macro.escape/1?">>)
end.
shallow_valid_ast(Expr) when is_list(Expr) -> shallow_valid_list(Expr);
shallow_valid_ast(Expr) -> shallow_valid_elem(Expr).
shallow_valid_list([]) -> true;
shallow_valid_list([Head | Tail]) -> shallow_valid_elem(Head) andalso shallow_valid_list(Tail);
shallow_valid_list(_Improper) -> false.
shallow_valid_elem(Expr) when is_list(Expr); is_atom(Expr); is_binary(Expr); is_number(Expr); is_pid(Expr); is_function(Expr) -> true;
shallow_valid_elem({Left, Right}) -> shallow_valid_elem(Left) andalso shallow_valid_elem(Right);
shallow_valid_elem({Atom, Meta, Args}) when is_atom(Atom), is_list(Meta), is_atom(Args) orelse is_list(Args) -> true;
shallow_valid_elem({Call, Meta, Args}) when is_list(Meta), is_list(Args) -> shallow_valid_ast(Call);
shallow_valid_elem(_Term) -> false.
%% TODO: We keep this with backwards compatibility in previous compiled Elixirw code.
shallow_validate_ast(Expr) ->
unquote(Expr).
argument_error(Message) ->
error('Elixir.ArgumentError':exception([{message, Message}])).
+11 -3
View File
@@ -147,7 +147,10 @@ inline(Mod, Fun, Arity) -> inner_inline(ex_to_erl, Mod, Fun, Arity).
?inline(?kernel, trunc, 1, erlang, trunc);
?inline(?kernel, tuple_size, 1, erlang, tuple_size);
?inline(?list, to_atom, 1, erlang, list_to_atom);
% we just want ex_to_erl, not erl_to_ex
inner_inline(ex_to_erl, ?list, to_atom, 1) -> {erlang, list_to_atom};
?inline(?list, to_unsafe_atom, 1, erlang, list_to_atom);
?inline(?list, to_existing_atom, 1, erlang, list_to_existing_atom);
?inline(?list, to_float, 1, erlang, list_to_float);
?inline(?list, to_integer, 1, erlang, list_to_integer);
@@ -205,8 +208,11 @@ inline(Mod, Fun, Arity) -> inner_inline(ex_to_erl, Mod, Fun, Arity).
?inline(?process, unlink, 1, erlang, unlink);
?inline(?process, unregister, 1, erlang, unregister);
% we just want ex_to_erl, not erl_to_ex
inner_inline(ex_to_erl, ?string, to_atom, 1) -> {erlang, binary_to_atom};
?inline(?string, duplicate, 2, binary, copy);
?inline(?string, to_atom, 1, erlang, binary_to_atom);
?inline(?string, to_unsafe_atom, 1, erlang, binary_to_atom);
?inline(?string, to_existing_atom, 1, erlang, binary_to_existing_atom);
?inline(?string, to_float, 1, erlang, binary_to_float);
?inline(?string, to_integer, 1, erlang, binary_to_integer);
@@ -238,7 +244,7 @@ inner_inline(_, _, _, _) -> false.
%%
%% Rewrite rules are more complex than regular inlining code
%% as they may change the number of arguments. However, they
%% cannot change tha argument order, as that can change the
%% cannot change the argument order, as that can change the
%% code semantics.
rewrite(?string_chars, DotMeta, to_string, Meta, [Arg]) ->
case is_always_string(Arg) of
@@ -328,6 +334,8 @@ guard(Receiver, DotMeta, Right, Meta, Args, S) ->
%% need to explicitly forbid as they are allowed in erl_internal.
allowed_guard(is_record, 2) -> false;
allowed_guard(is_record, 3) -> false;
allowed_guard('orelse', 2) -> true;
allowed_guard('andalso', 2) -> true;
allowed_guard(Right, Arity) ->
erl_internal:guard_bif(Right, Arity) orelse elixir_utils:guard_op(Right, Arity).
+6 -6
View File
@@ -1406,7 +1406,7 @@ tokenize_identifier(String, Line, Column, Scope, MaybeKeyword) ->
case suggest_simpler_unexpected_token_in_error(Wrong, Line, WrongColumn, Scope) of
no_suggestion ->
%% we append a pointer to more info if we aren't appending a suggestion
MoreInfo = "\nSee https://hexdocs.pm/elixir/unicode-syntax.html for more information.",
MoreInfo = "\nSee https://elixir.hexdocs.pm/unicode-syntax.html for more information.",
{error, {?LOC(Line, Column), {Prefix, Suffix ++ MoreInfo}, Wrong}};
{_, {Location, _, SuggestionMessage}} = _SuggestionError ->
@@ -1438,12 +1438,12 @@ suggest_simpler_unexpected_token_in_error(Wrong, Line, WrongColumn, Scope) ->
{error, _Reason} ->
ConfusableSkeleton = 'Elixir.String.Tokenizer.Security':confusable_skeleton(Wrong),
case (Scope#elixir_tokenizer.identifier_tokenizer):tokenize(ConfusableSkeleton) of
{_, Simpler, _, _, _, _} ->
{_, Simpler, _, _, _, _} when Simpler =/= Wrong ->
Message = suggest_change("Codepoint failed identifier tokenization, but a simpler form was found.",
Wrong,
"You could write the above in a similar way that is accepted by Elixir:",
Simpler,
"See https://hexdocs.pm/elixir/unicode-syntax.html for more information."),
"See https://elixir.hexdocs.pm/unicode-syntax.html for more information."),
{error, {?LOC(Line, WrongColumn), "unexpected token: ", Message}};
_other ->
no_suggestion
@@ -1453,7 +1453,7 @@ suggest_simpler_unexpected_token_in_error(Wrong, Line, WrongColumn, Scope) ->
Wrong,
"You could write the above in a compatible format that is accepted by Elixir:",
NFKC,
"See https://hexdocs.pm/elixir/unicode-syntax.html for more information."),
"See https://elixir.hexdocs.pm/unicode-syntax.html for more information."),
{error, {?LOC(Line, WrongColumn), "unexpected token: ", Message}}
end.
@@ -1918,13 +1918,13 @@ add_cursor(_Line, Column, noprune, Tokens) ->
{Column, Tokens};
add_cursor(Line, Column, {prune_and_cursor, true},
[{sigil, {_, _, {Line, Column}} = Location, Name, Parts, Modifier, Identation, Delimiter} | Rest]) ->
[{sigil, {_, _, {Line, Column}} = Location, Name, Parts, Modifier, Indentation, Delimiter} | Rest]) ->
Cursor = {'__cursor__', [{line, Line}, {column, Column}], []},
CursorModifier = case Modifier of
false -> Cursor;
List -> List ++ [Cursor]
end,
{Column + 12, [{sigil, Location, Name, Parts, CursorModifier, Identation, Delimiter} | Rest]};
{Column + 12, [{sigil, Location, Name, Parts, CursorModifier, Indentation, Delimiter} | Rest]};
add_cursor(Line, Column, {prune_and_cursor, _}, Tokens) ->
PrePrunedTokens = prune_identifier(Tokens),
+3 -4
View File
@@ -33,10 +33,6 @@ erl_fa_to_elixir_fa(Name, Arity) ->
_ -> {Name, Arity}
end.
guard_op('andalso', 2) ->
true;
guard_op('orelse', 2) ->
true;
guard_op(Op, Arity) ->
try erl_internal:op_type(Op, Arity) of
arith -> true;
@@ -233,6 +229,9 @@ returns_boolean({{'.', _, [erlang, Fun]}, _, [_, _]}) when
returns_boolean({{'.', _, [erlang, Fun]}, _, [_, _, _]}) when
Fun == function_exported; Fun == is_record -> true;
returns_boolean({{'.', _, [lists, member]}, _, [_, _]}) ->
true;
returns_boolean({'case', _, [_, [{do, Clauses}]]}) ->
lists:all(fun
({'->', _, [_, Expr]}) -> returns_boolean(Expr)
+55
View File
@@ -219,6 +219,61 @@ defmodule AccessTest do
end
end
describe "key/2 and key!/1" do
@test_map %{foo: :bar, baz: :qux}
test "finds key in map" do
assert get_in(@test_map, [Access.key(:foo)]) == :bar
assert get_in(@test_map, [Access.key(:missing)]) == nil
assert get_in(@test_map, [Access.key!(:foo)]) == :bar
assert_raise KeyError, fn ->
get_in(@test_map, [Access.key!(:missing)])
end
end
test "finds key in struct" do
defmodule KeySample do
defstruct foo: :bar, baz: :qux
end
assert get_in(struct(KeySample), [Access.key(:foo)]) == :bar
assert get_in(struct(KeySample), [Access.key(:missing)]) == nil
assert get_in(struct(KeySample), [Access.key!(:foo)]) == :bar
assert_raise KeyError, fn ->
get_in(struct(KeySample), [Access.key!(:missing)])
end
end
@test_keyword [foo: :bar, baz: :qux]
test "finds key in keyword list" do
assert get_in(@test_keyword, [Access.key(:foo)]) == :bar
assert get_in(@test_keyword, [Access.key(:missing)]) == nil
assert get_in(@test_keyword, [Access.key!(:foo)]) == :bar
assert_raise KeyError, fn ->
get_in(@test_keyword, [Access.key!(:missing)])
end
end
test "raises when key/2 access is attempted on [1,2,3]" do
assert_raise RuntimeError, ~r"Access.key/2 expected a map/struct/keyword list", fn ->
put_in([1, 2, 3], [Access.key(:foo)], :bar)
end
end
test "raises when key!/1 access is attempted on [1,2,3]" do
assert_raise RuntimeError, ~r"Access.key!/1 expected a map/struct/keyword list", fn ->
put_in([1, 2, 3], [Access.key!(:foo)], :bar)
end
end
end
describe "at/1" do
@test_list [1, 2, 3, 4, 5, 6]
+2 -2
View File
@@ -10,10 +10,10 @@ defmodule AtomTest do
doctest Atom, except: [:moduledoc]
test "to_string/1" do
assert "héllo" |> String.to_atom() |> Atom.to_string() == "héllo"
assert "héllo" |> String.to_unsafe_atom() |> Atom.to_string() == "héllo"
end
test "to_charlist/1" do
assert "héllo" |> String.to_atom() |> Atom.to_charlist() == ~c"héllo"
assert "héllo" |> String.to_unsafe_atom() |> Atom.to_charlist() == ~c"héllo"
end
end
+26
View File
@@ -340,6 +340,32 @@ defmodule CalendarTest do
assert Calendar.strftime(~N[2019-08-15 17:07:57], "%010A") == "00Thursday"
end
test "limits width to at most 1024 characters" do
assert Calendar.strftime(~D[2019-08-15], "%1024d") |> byte_size() == 1024
assert_raise ArgumentError, "invalid strftime format: width must be at most 1024", fn ->
Calendar.strftime(~D[2019-08-15], "%1025d")
end
assert_raise ArgumentError, "invalid strftime format: width must be at most 1024", fn ->
Calendar.strftime(~D[2019-08-15], "%10000d")
end
end
test "limits width in preferred formats" do
assert_raise ArgumentError, "invalid strftime format: width must be at most 1024", fn ->
Calendar.strftime(~N[2019-08-15 17:07:57], "%c", preferred_datetime: "%1025d")
end
assert_raise ArgumentError, "invalid strftime format: width must be at most 1024", fn ->
Calendar.strftime(~N[2019-08-15 17:07:57], "%x", preferred_date: "%1025d")
end
assert_raise ArgumentError, "invalid strftime format: width must be at most 1024", fn ->
Calendar.strftime(~N[2019-08-15 17:07:57], "%X", preferred_time: "%1025H")
end
end
test "formats Epoch time with %s" do
assert Calendar.strftime(~N[2019-08-15 17:07:57], "%s") == "1565888877"
+47
View File
@@ -514,6 +514,25 @@ defmodule CodeTest do
Code.unrequire_files([fixture_path("code_sample.exs")])
end
test "require_file/1 releases the file when compilation fails" do
path = tmp_path("bad_require_#{System.unique_integer([:positive])}.ex")
try do
File.write!(path, ~s|raise "boom"|)
assert_raise RuntimeError, "boom", fn ->
Code.require_file(path)
end
assert_raise RuntimeError, "boom", fn ->
Code.require_file(path)
end
after
File.rm(path)
Code.unrequire_files([path])
end
end
test "string_to_quoted!/2 errors take lines/columns/indentation into account" do
assert_exception(
SyntaxError,
@@ -898,6 +917,34 @@ defmodule Code.SyncTest do
Code.put_compiler_option(:module_definition, :compiled)
end
if System.otp_release() >= "29" do
test "uses erlc_options compiler option" do
module = CodeTest.ConfiguredBeamDebugInfo
previous = Code.compiler_options(erlc_options: [:beam_debug_info])
try do
assert [{^module, binary}] =
Code.compile_string("""
defmodule CodeTest.ConfiguredBeamDebugInfo do
def sample(value) do
doubled = value * 2
doubled + 1
end
end
""")
assert {:ok, {_, [{~c"DbgB", <<_version::32, entries::32, _::binary>>}]}} =
:beam_lib.chunks(binary, [~c"DbgB"])
assert entries > 0
after
Code.compiler_options(previous)
:code.purge(module)
:code.delete(module)
end
end
end
test "prepend_path" do
path = Path.join(__DIR__, "fixtures")
true = Code.prepend_path(path)
+2 -2
View File
@@ -98,12 +98,12 @@ defmodule EnumTest do
end
test "chunk/3" do
enum = String.to_atom("Elixir.Enum")
enum = String.to_unsafe_atom("Elixir.Enum")
assert enum.chunk(1..5, 2, 1) == Enum.chunk_every(1..5, 2, 1, :discard)
end
test "chunk/4" do
enum = String.to_atom("Elixir.Enum")
enum = String.to_unsafe_atom("Elixir.Enum")
assert enum.chunk(1..5, 2, 1, nil) == Enum.chunk_every(1..5, 2, 1, :discard)
end
+1 -1
View File
@@ -210,7 +210,7 @@ defmodule ExceptionTest do
assert Exception.format_mfa(Foo, :こんにちは世界, [1, 2]) == "Foo.こんにちは世界(1, 2)"
nfd = :unicode.characters_to_nfd_binary("olá")
assert Exception.format_mfa(Foo, String.to_atom(nfd), [1, 2]) == "Foo.\"#{nfd}\"(1, 2)"
assert Exception.format_mfa(Foo, String.to_unsafe_atom(nfd), [1, 2]) == "Foo.\"#{nfd}\"(1, 2)"
end
test "format_fa/2" do

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