Skip to content

Commit

Permalink
Verify the right side of binary generators
Browse files Browse the repository at this point in the history
  • Loading branch information
josevalim committed Sep 23, 2024
1 parent 13cab5f commit 261a3ed
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 41 deletions.
47 changes: 34 additions & 13 deletions lib/elixir/lib/module/types/expr.ex
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ defmodule Module.Types.Expr do
# TODO: left = right
def of_expr({:=, _meta, [left_expr, right_expr]} = expr, stack, context) do
with {:ok, right_type, context} <- of_expr(right_expr, stack, context) do
Pattern.of_pattern(left_expr, {right_type, expr}, stack, context)
Pattern.of_match(left_expr, {right_type, expr}, stack, context)
end
end

Expand Down Expand Up @@ -415,22 +415,22 @@ defmodule Module.Types.Expr do
defp for_clause({:<-, meta, [left, expr]}, stack, context) do
{pattern, guards} = extract_head([left])

with {:ok, _pattern_type, context} <-
with {:ok, _expr_type, context} <- of_expr(expr, stack, context),
{:ok, _pattern_type, context} <-
Pattern.of_head([pattern], guards, meta, stack, context),
{:ok, _expr_type, context} <- of_expr(expr, stack, context),
do: {:ok, context}
end

defp for_clause({:<<>>, _, [{:<-, _, [pattern, expr]}]}, stack, context) do
# TODO: the compiler guarantees pattern is a binary but we need to check expr is a binary
with {:ok, _pattern_type, context} <-
Pattern.of_pattern(pattern, stack, context),
{:ok, _expr_type, context} <- of_expr(expr, stack, context),
do: {:ok, context}
end

defp for_clause(list, stack, context) when is_list(list) do
reduce_ok(list, context, &for_option(&1, stack, &2))
defp for_clause({:<<>>, _, [{:<-, meta, [left, right]}]}, stack, context) do
with {:ok, right_type, context} <- of_expr(right, stack, context),
{:ok, _pattern_type, context} <- Pattern.of_match(left, {binary(), left}, stack, context) do
if binary_type?(right_type) do
{:ok, context}
else
warning = {:badbinary, right_type, right, context}
{:ok, warn(__MODULE__, warning, meta, stack, context)}
end
end
end

defp for_clause(expr, stack, context) do
Expand Down Expand Up @@ -559,4 +559,25 @@ defmodule Module.Types.Expr do
])
}
end

def format_diagnostic({:badbinary, type, expr, context}) do
traces = Of.collect_traces(expr, context)

%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
expected the right side of <- in a binary generator to be a binary:
#{expr_to_string(expr) |> indent(4)}
but got type:
#{to_quoted_string(type) |> indent(4)}
""",
Of.format_traces(traces)
])
}
end
end
55 changes: 27 additions & 28 deletions lib/elixir/lib/module/types/pattern.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,31 +21,30 @@ defmodule Module.Types.Pattern do
do: {:ok, types, context}
end

## Patterns

@doc """
Return the type and typing context of a pattern expression
with no {expected, expr} pair. of_pattern/4 must be preferred
whenever possible as it adds more context to errors.
Return the type and typing context of a pattern expression with
the given {expected, expr} pair or an error in case of a typing conflict.
"""
def of_pattern(expr, stack, context) do
def of_match(expr, expected_expr, stack, context) do
of_pattern(expr, expected_expr, stack, context)
end

## Patterns

defp of_pattern(expr, stack, context) do
# TODO: Remove the hardcoding of dynamic
# TODO: Possibly remove this function
of_pattern(expr, {dynamic(), expr}, stack, context)
end

@doc """
Return the type and typing context of a pattern expression with
the given {expected, expr} pair or an error in case of a typing conflict.
"""

# ^var
def of_pattern({:^, _meta, [var]}, expected_expr, stack, context) do
defp 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
defp of_pattern({:=, _meta, [left_expr, right_expr]}, {expected, expr}, stack, context) do
case {is_var(left_expr), is_var(right_expr)} do
{true, false} ->
with {:ok, type, context} <- of_pattern(right_expr, {expected, expr}, stack, context) do
Expand All @@ -65,13 +64,13 @@ defmodule Module.Types.Pattern do
end

# %var{...} and %^var{...}
def of_pattern(
{:%, _meta, [struct_var, {:%{}, _meta2, args}]} = expr,
expected_expr,
stack,
context
)
when not is_atom(struct_var) do
defp of_pattern(
{:%, _meta, [struct_var, {:%{}, _meta2, args}]} = expr,
expected_expr,
stack,
context
)
when not is_atom(struct_var) do
with {:ok, struct_type, context} <-
of_pattern(struct_var, {atom(), expr}, %{stack | refine: false}, context),
{:ok, map_type, context} <-
Expand All @@ -84,35 +83,35 @@ defmodule Module.Types.Pattern do
end

# %Struct{...}
def of_pattern({:%, _meta, [module, {:%{}, _, args}]} = expr, expected_expr, stack, context)
when is_atom(module) do
defp of_pattern({:%, _meta, [module, {:%{}, _, args}]} = expr, expected_expr, stack, context)
when is_atom(module) do
with {:ok, actual, context} <-
Of.struct(expr, module, args, :merge_defaults, stack, context, &of_pattern/3) do
Of.intersect(actual, expected_expr, stack, context)
end
end

# %{...}
def of_pattern({:%{}, _meta, args}, expected_expr, stack, context) do
defp of_pattern({:%{}, _meta, args}, expected_expr, stack, context) do
of_open_map(args, [], expected_expr, stack, context)
end

# <<...>>>
def of_pattern({:<<>>, _meta, args}, _expected_expr, stack, context) do
defp of_pattern({:<<>>, _meta, args}, _expected_expr, stack, context) do
case Of.binary(args, :pattern, stack, context, &of_pattern/4) do
{:ok, context} -> {:ok, binary(), context}
{:error, context} -> {:error, context}
end
end

# _
def of_pattern({:_, _meta, _var_context}, {expected, _expr}, _stack, context) do
defp of_pattern({:_, _meta, _var_context}, {expected, _expr}, _stack, context) do
{:ok, expected, context}
end

# var
def of_pattern({name, meta, ctx} = var, {expected, expr}, stack, context)
when is_atom(name) and is_atom(ctx) do
defp 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)
Expand All @@ -130,7 +129,7 @@ defmodule Module.Types.Pattern do
end
end

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

Expand Down
24 changes: 24 additions & 0 deletions lib/elixir/test/elixir/module/types/expr_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -527,4 +527,28 @@ defmodule Module.Types.ExprTest do
"""}
end
end

describe "comprehensions" do
test "binary generators" do
assert typewarn!([<<x>>], for(<<y <- x>>, do: y)) ==
{dynamic(),
~l"""
expected the right side of <- in a binary generator to be a binary:
x
but got type:
integer()
where "x" was given the type:
# type: integer()
# from: types_test.ex:533
<<x>>
#{hints(:inferred_bitstring_spec)}
"""}
end
end
end

0 comments on commit 261a3ed

Please sign in to comment.