Skip to content

Commit

Permalink
Improve error messages when matching on size
Browse files Browse the repository at this point in the history
  • Loading branch information
josevalim committed Sep 27, 2024
1 parent d127196 commit 1f92332
Show file tree
Hide file tree
Showing 6 changed files with 53 additions and 42 deletions.
4 changes: 1 addition & 3 deletions lib/elixir/lib/module/types.ex
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,7 @@ defmodule Module.Types do
# List of calls to not warn on as undefined
no_warn_undefined: no_warn_undefined,
# A list of cached modules received from the parallel compiler
cache: cache,
# If variable refinements is enabled or not
refine: true
cache: cache
}
end

Expand Down
4 changes: 3 additions & 1 deletion lib/elixir/lib/module/types/expr.ex
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,9 @@ defmodule Module.Types.Expr do
{"rescue #{expr_to_string(expr)} ->", hints}
end

{:ok, _type, context} = Of.refine_var(var, expected, expr, formatter, stack, context)
{:ok, _type, context} =
Of.refine_var(var, {expected, expr}, formatter, stack, context)

context
end

Expand Down
14 changes: 7 additions & 7 deletions lib/elixir/lib/module/types/of.ex
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ defmodule Module.Types.Of do
@doc """
Refines the type of a variable.
"""
def refine_var(var, type, expr, formatter \\ :default, stack, context) do
def refine_var(var, {type, expr}, formatter \\ :default, stack, context) do
{var_name, meta, var_context} = var
version = Keyword.fetch!(meta, :version)

Expand Down Expand Up @@ -251,7 +251,7 @@ defmodule Module.Types.Of do
end

with {:ok, _type, context} <- result do
{:ok, specifier_size(kind, right, stack, context)}
{:ok, specifier_size(kind, right, expr, stack, context)}
end
end

Expand All @@ -271,27 +271,27 @@ defmodule Module.Types.Of do
defp specifier_type(_kind, {:binary, _, _}), do: @binary
defp specifier_type(_kind, _specifier), do: @integer

defp specifier_size(kind, {:-, _, [left, right]}, stack, context) do
specifier_size(kind, right, stack, specifier_size(kind, left, stack, context))
defp specifier_size(kind, {:-, _, [left, right]}, expr, stack, context) do
specifier_size(kind, right, expr, stack, specifier_size(kind, left, expr, stack, context))
end

defp specifier_size(:expr, {:size, _, [arg]} = expr, stack, context)
defp specifier_size(:expr, {:size, _, [arg]}, expr, stack, context)
when not is_integer(arg) do
case Module.Types.Expr.of_expr(arg, {integer(), expr}, stack, context) do
{:ok, _, context} -> context
{:error, context} -> context
end
end

defp specifier_size(_pattern_or_guard, {:size, _, [arg]} = expr, stack, context)
defp specifier_size(_pattern_or_guard, {:size, _, [arg]}, expr, stack, context)
when not is_integer(arg) do
case Module.Types.Pattern.of_guard(arg, {integer(), expr}, stack, context) do
{:ok, _, context} -> context
{:error, context} -> context
end
end

defp specifier_size(_kind, _expr, _stack, context) do
defp specifier_size(_kind, _specifier, _expr, _stack, context) do
context
end

Expand Down
63 changes: 34 additions & 29 deletions lib/elixir/lib/module/types/pattern.ex
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,6 @@ defmodule Module.Types.Pattern do
of_pattern(expr, {dynamic(), expr}, stack, context)
end

# ^var
def of_pattern({:^, _meta, [var]}, expected_expr, stack, context) do
Of.intersect(Of.var(var, context), expected_expr, stack, context)
end

# left = right
# TODO: Track variables and handle nesting
def of_pattern({:=, _meta, [left_expr, right_expr]}, {expected, expr}, stack, context) do
Expand Down Expand Up @@ -73,7 +68,7 @@ defmodule Module.Types.Pattern do
)
when not is_atom(struct_var) do
with {:ok, struct_type, context} <-
of_pattern(struct_var, {atom(), expr}, %{stack | refine: false}, context),
of_struct_var(struct_var, {atom(), expr}, stack, context),
{:ok, map_type, context} <-
of_open_map(args, [__struct__: struct_type], expected_expr, stack, context),
{_, struct_type} = map_fetch(map_type, :__struct__),
Expand Down Expand Up @@ -111,23 +106,8 @@ defmodule Module.Types.Pattern do
end

# var
def of_pattern({name, meta, ctx} = var, {expected, expr}, stack, context)
when is_atom(name) and is_atom(ctx) do
case stack do
%{refine: true} ->
Of.refine_var(var, expected, expr, stack, context)

%{refine: false} ->
version = Keyword.fetch!(meta, :version)

case context do
%{vars: %{^version => %{type: type}}} ->
Of.intersect(type, {expected, expr}, stack, context)

%{} ->
{:ok, expected, context}
end
end
def of_pattern(var, expected_expr, stack, context) when is_var(var) do
Of.refine_var(var, expected_expr, stack, context)
end

def of_pattern(expr, expected_expr, stack, context) do
Expand Down Expand Up @@ -198,21 +178,39 @@ defmodule Module.Types.Pattern do
end
end

# ^var
# Happens from inside size(^...) and map keys
def of_guard({:^, _meta, [var]}, expected_expr, stack, context) do
Of.intersect(Of.var(var, context), expected_expr, stack, context)
end

# var
def of_guard(var, expected_expr, stack, context) when is_var(var) do
# TODO: This should be ver refinement once we have inference in guards
# Of.refine_var(var, expected_expr, stack, context)
Of.intersect(Of.var(var, context), expected_expr, stack, context)
end

def of_guard(expr, expected_expr, stack, context) do
of_shared(expr, expected_expr, stack, context, &of_guard/4)
end

## Helpers

defp of_struct_var({:_, _, _}, {expected, _expr}, _stack, context) do
{:ok, expected, context}
end

defp of_struct_var({:^, _, [var]}, expected_expr, stack, context) do
Of.intersect(Of.var(var, context), expected_expr, stack, context)
end

defp of_struct_var({_name, meta, _ctx}, expected_expr, stack, context) do
version = Keyword.fetch!(meta, :version)

case context do
%{vars: %{^version => %{type: type}}} ->
Of.intersect(type, expected_expr, stack, context)

%{} ->
{:ok, elem(expected_expr, 0), context}
end
end

## Shared

# :atom
Expand Down Expand Up @@ -269,6 +267,12 @@ defmodule Module.Types.Pattern do
of_shared({:{}, [], [left, right]}, expected_expr, stack, context, fun)
end

# ^var
defp of_shared({:^, _meta, [var]}, expected_expr, stack, context, _fun) do
# This is by definition a variable defined outside of this pattern, so we don't track it.
Of.intersect(Of.var(var, context), expected_expr, stack, context)
end

# left | []
defp of_shared({:|, _meta, [left_expr, []]}, _expected_expr, stack, context, fun) do
fun.(left_expr, {dynamic(), left_expr}, stack, context)
Expand Down Expand Up @@ -303,6 +307,7 @@ defmodule Module.Types.Pattern do
end

# {...}
# TODO: Implement this
defp of_shared({:{}, _meta, exprs}, _expected_expr, stack, context, fun) do
case map_reduce_ok(exprs, context, &fun.(&1, {dynamic(), &1}, stack, &2)) do
{:ok, types, context} -> {:ok, tuple(types), context}
Expand Down
8 changes: 7 additions & 1 deletion lib/elixir/test/elixir/module/types/expr_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ defmodule Module.Types.ExprTest do
~l"""
incompatible types in expression:
size(x)
<<y::integer-size(x)>>
expected type:
Expand All @@ -225,6 +225,12 @@ defmodule Module.Types.ExprTest do
# type: binary()
# from: types_test.ex:LINE-2
<<x::binary>>
where "y" was given the type:
# type: dynamic()
# from: types_test.ex:208
y
"""}
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/elixir/test/elixir/module/types/pattern_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ defmodule Module.Types.PatternTest do
~l"""
incompatible types in expression:
size(x)
<<..., _::integer-size(x)>>
expected type:
Expand Down

0 comments on commit 1f92332

Please sign in to comment.