Return type of Map.pop is too narrow · Issue #15482 · elixir-lang/elixir · GitHub
Skip to content

Return type of Map.pop is too narrow #15482

Closed
Closed

Description

@m1dnight

Existing issue

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

Elixir and Erlang/OTP versions

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

Elixir 1.20.1 (compiled with Erlang/OTP 29)

Operating system

MacOS Tahoe 26.1

Current behavior

defmodule Repro do
 # pop an element from a non-empty map
  def pop(map, key) when map_size(map) > 0 do
    {_value, rest} = Map.pop!(map, key)
    rest == %{}
  end
  
  def test do
    map = %{"some_key" => 1}
    pop(map, "some_key")
  end
end

Generates the following compiler output:

    warning: comparison between distinct types found:

        rest == %{}

    given types:

        dynamic(map() and not empty_map()) == empty_map()

    where "rest" was given the type:

        # type: dynamic(map() and not empty_map())
        # from: bug.ex:3:20
        {_value, rest} = Map.pop!(map, key)

    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

    type warning found at:
    │
  4 │     rest == %{}
    │          ~
    │
    └─ bug.ex:4:10: Repro.pop/2

Expected behavior

The compiler correctly marks the type of the map value as dynamic(map() and not empty_map()), but after calling Map.pop!/2 the type of rest can be empty_map(), so I think the right type is supposed to be just map().

I have found this PR: #15326, but I do not think they are the same. This one is for a tautological guard (checking a map size is bigger or equal to 0), and this one is not.

Activity

  1. josevalim commented on Jun 15, 2026

    @josevalim
    Member

    These are more isolated tests:

    diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs
    index b48049f2c..12b074eca 100644
    --- a/lib/elixir/test/elixir/module/types/descr_test.exs
    +++ b/lib/elixir/test/elixir/module/types/descr_test.exs
    @@ -2712,6 +2712,13 @@ test "with non-empty open maps does not call the callback with none from absent
           refute_received :callback_invoked
         end
    
    +    test "deleting domain keys from non-empty maps may return an empty map" do
    +      type = dynamic(opt_difference(open_map(), empty_map()))
    +
    +      assert {_value, descr, []} = map_update(type, binary(), not_set(), true, false)
    +      assert subtype?(empty_map(), descr)
    +    end
    +
         test "with dynamic atom keys" do
           assert {type, descr, errors} =
                    map_update(closed_map(key: atom([:value])), dynamic(), atom([:new_value]))
    diff --git a/lib/elixir/test/elixir/module/types/map_test.exs b/lib/elixir/test/elixir/module/types/map_test.exs
    index d871af0a9..1e206673a 100644
    --- a/lib/elixir/test/elixir/module/types/map_test.exs
    +++ b/lib/elixir/test/elixir/module/types/map_test.exs
    @@ -804,6 +804,15 @@ test "checking" do
    
           assert typecheck!([x], Map.pop!(x, 123)) ==
                    dynamic(tuple([term(), open_map()]))
    +
    +      assert typecheck!(
    +               [x],
    +               map_size(x) > 0,
    +               (
    +                 {_value, rest} = Map.pop!(x, "some_key")
    +                 rest
    +               )
    +             ) == dynamic(open_map())
         end
    
         test "inference" do
  2. added theissue type on Jun 15, 2026
  3. added a commit that references this issue on Oct 26, 2026
    c20f958
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions