Skip to content

Typechecker issues with numberize #15799

Description

@lukaszsamson

Existing issue

  • I have searched existing issues and could not find a duplicate.

Elixir and Erlang/OTP versions

Erlang/OTP 28 [erts-16.4.0.1] [source] [64-bit] [smp:12:12] [ds:12:12:10] [async-threads:1] [jit]

Elixir 1.21.0-dev (a4ab118) (compiled with Erlang/OTP 28)

Operating system

any

Current behavior

  1. numberize/1 rewrites negated BDD leaves so so == guard narrowing on difference types excludes values equal at runtime

Repro:

    defmodule M do
      def check(v, q) do
        other =
          case v do
            {a} when is_float(a) -> {:float, a}   # clause subtraction gives
            other -> other                                  # other : ... and not {float()}
          end
        w = if q, do: {1}, else: {1.5}
        case w do
          x when x == other -> {:matched, x}
          _ -> :nomatch
        end
      end
    end
    M.check({1}, true)   # => {:matched, {1}}

emits false positive warning:

warning: the following pattern will never match:

    x == other

where "other" was given the type:

    # type: dynamic(
      ({...} and not {float()}) or atom() or bitstring() or empty_list() or float() or fun() or
        integer() or map() or non_empty_list(term(), term()) or pid() or port() or reference() or
        {:float, float()}
    )
    # from: iex:3
    other =
      case v do
        ...
      end

└─ iex:10: M.check/2

After numberization, not {float()} becomes not {number()}

  1. numberize(:map, _) skips domain value types, so == on integer-keyed maps narrows to types excluding equal maps

Repro:

    defmodule M do
      def check do
        y = %{1 => 1}
        w = %{1 => 1.0}
        case w do
          x when x == y and map_size(x) == 1 -> {:eq, x}
          _ -> :ne
        end
      end
    end
    M.check()   # => {:eq, %{1 => 1.0}}

emits false positive:

warning: incompatible types assigned to "y":

    %{integer() => integer()} !~ %{integer() => float()} and not empty_map()

where "y" was given the types:

    # type: %{integer() => integer()}
    # from: iex:16
    y = %{1 => 1}

    # type: %{integer() => float()} and not empty_map()
    # from: iex:19
    x == y

└─ iex:19: M.check/0

numberize should also map the domain value types inside the leaf tag

  1. comparison between distinct types ignores coercion inside containers
    NOTE: this is acknowledged in code comments but it's relatively easy to produce code triggering that

Repro:

defmodule M3 do
               def check(q) do
                 a = if q, do: {1}, else: {2}
             b = {1.0}
             case :ok do
               :ok when a == b -> :eq
               _ -> :ne
             end
           end
         end

false positive:

warning: comparison between distinct types found:

    a == b

given types:

    dynamic({integer()}) == {float()}

where "a" was given the type:

    # type: dynamic({integer()})
    # from: iex:24
    a =
      if q do
        {1}
      else
        {2}
      end

where "b" was given the type:

    # type: {float()}
    # from: iex:25
    b = {1.0}

While Elixir can compare across all types, you are comparing across types which are always disjoint, and the result is either always true or always false

└─ iex:27: M3.check/1

Expected behavior

Issues 1 and 2 should be addressed; 3 might be an intended tradeoff

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions