gradient icon indicating copy to clipboard operation
gradient copied to clipboard

Clause unreachable for `integer` return value rather than `any`

Open baldwindavid opened this issue 3 years ago • 2 comments

I'm testing out gradient on a https://github.com/baldwindavid/ex_waiter/pull/3 and get an error that a clause cannot be reached when setting the return type as an integer on a function. It does work if the return type is any. The function and desired spec is commented out at... https://github.com/baldwindavid/ex_waiter/pull/3/files#diff-a502442c0bd637c012b9c3bd3788d431af95331497257e34e1a53bb8a5446f49R332

baldwindavid avatar May 26 '22 15:05 baldwindavid

There are two remaining FIXMEs in the linked PR, let me go over them one by one.

  # FIXME: It seems the second attempt/1 clause cannot be reached.
  @spec attempt(%Waiter{}) :: {:ok, %Waiter{}} | {:error, %Waiter{}}
  defp attempt(%Waiter{attempt_num: num, num_attempts: num} = waiter) do

This one seems to be caused by a known limitation in the type checker, i.e. by it not being able to understand variable binds in the struct pattern and the fact that both fields must match. In other words, the problem is on the type checker side, not in the checked code, so it's a false positive.

  # FIXME: Error when the return type is set to integer()
  # Error: lib/ex_waiter.ex: The clause on line XXX cannot be reached
  @spec delay_default(%Waiter{}) :: timeout()
  defp delay_default(%Waiter{} = waiter) do
    waiter.attempt_num * 10
  end

This one seems to be an issue we were not aware of before. Thanks, @baldwindavid!

Details follow - delay_default and its spec are translated to the following Erlang:

168 -spec delay_default(#{'__struct__' :=
169                           'Elixir.ExWaiter.Waiter',
170                       attempt_num := term(), attempts := term(),
171                       checker_fn := term(), delay := term(),
172                       'exception_on_retries_exhausted?' := term(),
173                       'fulfilled?' := term(), num_attempts := term(),
174                       on_failure := term(), on_success := term(),
175                       returning := term(), total_delay := term(),
176                       value := term()}) -> timeout().

276 delay_default(#{'__struct__' :=
277                     'Elixir.ExWaiter.Waiter'} =
278                   _waiter@1) ->
279     case _waiter@1 of
280         #{attempt_num := _@1} -> _@1;
281         _@1 when erlang:is_map(_@1) ->
282             erlang:error({badkey, attempt_num, _@1});
283         _@1 -> _@1:attempt_num()
284     end
285         * 10.

When type checking that as Erlang the error message pinpoints the exact clause that cannot match:

Typechecking files...
src/ex_waiter.erl: The clause on line 281 at column 9 cannot be reached

This means we're not type checking the generated code properly. Thanks again, @baldwindavid, for taking the time to try Gradient and Gradualizer out. We've got some homework to do based on this report :)

erszcz avatar May 27 '22 15:05 erszcz

Thanks for checking into the report! This was the perfect size small codebase to try out the package and it works quite well. I'm looking forward to seeing it develop and hope to use it on a large codebase at some point. I've mostly been using the TypeCheck library, which is very powerful, but just really hard to beat a static type checker that uses regular Elixir typespecs.

baldwindavid avatar May 27 '22 20:05 baldwindavid