gradient icon indicating copy to clipboard operation
gradient copied to clipboard

`if` expression support and type refinement

Open eksperimental opened this issue 2 years ago • 3 comments

I have

  @spec child_pid(term) :: pid | nil
  def child_pid(term) when not is_pid(term) do
    pid({:child, term})
  end

  def child_pid(pid) do
    case Supervisor.which_children(pid) do
      [{_id, child_pid, _type, _modules}] ->
        if is_pid(child_pid) do
          child_pid
        else
          nil
        end

      _ ->
        nil
    end
  end

I'm getting:

lib/checker/util.ex: The variable on line 59 is expected to have type pid() | nil but it has type :restarting | :undefined | pid()
57       [{_id, child_pid, _type, _modules}] ->
58         if is_pid(child_pid) do
59           child_pid
60         else
61           nil

I don't think it should get any error,

Rewritting the clause like this makes the error go away, and it is clearer, but I thought of sharing it here anyway.

  def child_pid(pid) do
    case Supervisor.which_children(pid) do
      [{_id, child_pid, _type, _modules}] when is_pid(child_pid) ->
        child_pid

      _ ->
        nil
    end
  end

eksperimental avatar May 12 '22 01:05 eksperimental

Thanks, @eksperimental, good catch! The Elixir if is compiled to an Erlang case and it seems something is too quirky in the resulting code to be type checked properly. Marking it as a bug.

erszcz avatar May 12 '22 09:05 erszcz

Not sure if this belongs here or in a new issue. It seems to be potentially the same issue.

@spec handle(pos_integer() | nil) :: :ok
def handle(id) do
  if id != nil do
    # This has an error complaining about nil.
    do_something(id)
  end
  
  case id do
    id when id != nil ->
      # This is correctly narrowed to just pos_integer().
      do_something(id)
      
    _ ->
      nil
  end
  
  # This is how elixir seems to compile the above if expression.
  case id != nil do
    false ->
      nil
    
    true ->
      # This also fails like the above if.
      do_something(id)
  end
  
  :ok
end
  
@spec do_something(pos_integer()) :: :ok
def do_something(id), do: id

Does Gradualizer only work with guards for refining the type?

hworld avatar Mar 13 '23 21:03 hworld

Hi, @hworld!

Thanks for your interest and another example. Indeed, it seems to be another case of the same issue. We're aware that refinement is still somewhat limited and this seems to one of the cases where it's clearly visible.

erszcz avatar Mar 14 '23 08:03 erszcz