Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions lib/elixir/lib/module/types/expr.ex
Original file line number Diff line number Diff line change
Expand Up @@ -347,9 +347,9 @@ defmodule Module.Types.Expr do
cache_result(meta, stack, context, fn ->
{body_type, acc_context} =
reduce_non_empty(clauses, {none(), context}, fn
{:->, meta, [[head], body]}, {acc, context}, last? ->
{:->, meta, [[head], body]}, {acc, initial_context}, last? ->
{head_type, context} =
of_expr(head, term(), head, %{stack | reverse_arrow: :cache}, context)
of_expr(head, term(), head, %{stack | reverse_arrow: :cache}, initial_context)

context =
maybe_always_or_never_match_cond(head_type, head, meta, stack, context, last?)
Expand All @@ -360,9 +360,8 @@ defmodule Module.Types.Expr do
# Keep the context except the warnings, and compute the body
truthy_context = reset_warnings(truthy_context, context)
{body_type, body_context} = of_expr(body, expected, expr, stack, truthy_context)

# Reset the context vars to the head definition to compute the falsy type
context = Of.reset_vars(body_context, context)
context = Of.reset_vars(body_context, initial_context)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This context of_expr(head, term(), head, %{stack | reverse_arrow: :cache}, initial_context) is a broad context, it should not be forcing the variable x to be a tuple unless we are considering that both sides of is_tuple(x) and tuple_size(x) == 2 are executed or there is another bug.

This only happens with defguard?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be clear, we are not expecting to be a tuple because the expected type is term() and not @truthy.

@sabiwara sabiwara Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@josevalim you're right, this is not a defguard thing but an andalso one (which unlike and - which is case - isn't typed as a lazy construct, as it should).

   cond do
      :erlang.andalso(is_tuple(x), tuple_size(x) == 2) -> :pair
      is_atom(x) -> :atom
    end

The problem is not with cond itself. Will clause this PR.
Although variables defined in the clause head like arg1 semantically shouldn't be leaking to the next clause, so I still think there might be something incorrect about context management.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@josevalim I think the correct approach is to add a specific clause for :erlang.orelse / :erlang.andalso in expr.ex, right?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's exactly the root cause, we are considering the right-hand side is always executed. I will look into a fix because unfortunately it is not that trivial, otherwise we can end-up with :erlang.orelse(foo, bar) having more precision than Kernel.or(foo, bar) or Kernel.||(foo, bar).


context =
if last? do
Expand Down
12 changes: 12 additions & 0 deletions lib/elixir/test/elixir/module/types/expr_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3450,5 +3450,17 @@ defmodule Module.Types.ExprTest do
)
) == dynamic() or binary()
end

defguard is_pair(x) when is_tuple(x) and tuple_size(x) == 2

test "cond with custom guard" do
assert typecheck!(
[x],
cond do
is_pair(x) -> :pair
is_atom(x) -> :atom
end
) == atom([:pair, :atom])
end
end
end
Loading