Skip to content

Properly reset variables between cond clauses#15459

Closed
sabiwara wants to merge 1 commit into
elixir-lang:mainfrom
sabiwara:cond-false-positive
Closed

Properly reset variables between cond clauses#15459
sabiwara wants to merge 1 commit into
elixir-lang:mainfrom
sabiwara:cond-false-positive

Conversation

@sabiwara

@sabiwara sabiwara commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Close #15450 (tentative fix)

Explanation:

In initial_context, we just have x. But the guard expands to:

{arg1} = {x}
:erlang.andalso(:erlang.is_tuple(arg1), :erlang.==(:erlang.tuple_size(arg1), 2))

So after of_expr, we added arg1 to the context which doesn't get reset by reset_vars.
Not sure yet why it actually influences x though, still investigating.

To be backported.


# 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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Type system guard refinement false positive in cond with custom guard

2 participants