This pull request adds inference of patterns, being able to
refine types as new information is added. The next step is
to use this inference in the compiler itself.
Future work will add module-local inference of return types
(most likely still in v1.18) and inference of guards (most
likely in v1.19).
In order to support inference of patterns, Elixir will raise
if it finds recursive variable definitions. This means
patterns that never match, such as this one, will no longer
compile:
def foo(x = {:ok, y}, x = y)
However, recursion of root variables (where variables directly
point to each other), will also fail to compile:
def foo(x = y, y = z, z = x)
While the definition above could succeed (as long as all three
arguments are equal), there is a much cleaner version of writing
the same code, that does not require solving cycles in our head:
def foo(x, x, x)
Previously, the internal Elixir expansion pass worked directly on
Macro.Env. However, this poised an issue, if we want to track
more information in the pass, we ended up exposing it on
Macro.Env, making it larger, and potentially slowing down
operations such as __ENV__ serialization.
This commit refactors the expansion pass to work with two
structures, the Macro.Env struct and a #elixir_ex{} record.
On OTP 24:
iex> <<x::float-16>> = <<60, 0>>
iex> x
1.0
iex> <<x::float-16>>
<<60, 0>>
Before OTP 24 we'd get errors or wouldn't match:
iex> <<1.0::float-16>>
** (ArgumentError) argument error while evaluating iex at line 1
<<x::float-16>> = <<60, 0>>
** (MatchError) no match of right hand side value: <<60, 0>>
iex> (fn <<x::float-16>> -> x; _ -> :nomatch end).(<<60, 0>>)
:nomatch
During the bitstring expansion, guard checks were avoided due to
a special case for inlining binaries during interpolation. These
changes ensure guards are checked.
Binary/bitstring matching allows to dynamically define the
`size` of the binary in certain conditions:
* if the `size` variable is defined prior to the pattern
match:
iex> size = 8
iex> <<a::size(size), rest::binary>> = "hello"
iex> a
104
* if the `size` variable is matched within the same
binary/bitstring match, prior to its use:
iex> <<name_size::size(8), name::binary-size(name_size), _rest::binary>> = <<5, "Frank the Walrus">>
iex> name
"Frank"
Other cases are considered illegal patterns, for example:
{name_size, <<name::binary-size(name_size), _rest::binary>>} = {5, "Frank the Walrus"}
This commit raises a `CompileError: undefined variable ...` for such cases.
This gives us better control over when and how unused variables are printed.
As a result, we are able to emit unused variable warnings in situations
we could not before. This also opens up the way for us to remove a
dependency on erl_lint and track types information, which allows us to
speed up compilation times about 5% and allow us to emit more performant
code in some situations.