Skip to content

Map update propagates replacement type refinement to the original map #15882

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]

Interactive Elixir (1.21.0-dev)

Operating system

any

Current behavior

Map update incorrectly refines original map fields with replacement type constraints

Repro 1:

    defmodule MapOverwrite do
      def replace(m), do: %{m | a: 1}.a + 1
      def run, do: replace(%{a: :old})

      def original(m) do
        %{a: 1} = %{m | a: 1}

        case m.a do
          :old -> :kept_original
          _ -> :other
        end
      end
    end

    IO.inspect(MapOverwrite.run())
    IO.inspect(MapOverwrite.original(%{a: :old}))

Result: code works fine and prints:

2
:kept_original

but raises 2 false positive warnings:

warning: incompatible types given to replace/1:

    replace(%{a: :old})

given types:

    %{a: :old}

but expected one of:

    %{..., a: float() or integer()}

└─ iex:3: MapOverwrite.run/0

warning: the following clause will never match:

    :old ->

because it attempts to match on the result of:

    m.a

which has type:

    integer()

└─ iex:9: MapOverwrite.original/1

Another repro:

   defmodule MapOverwriteCase do
      def original(m) do
        case %{m | a: 1} do
          %{a: 1} -> :ok
        end

        case m.a do
          :old -> :kept_original
          _ -> :other
        end
      end

      def domain(m) do
        case %{m | a: 1} do
          %{a: 1} -> :ok
        end
      end

      def run, do: domain(%{a: :old})
    end

    IO.inspect(MapOverwriteCase.original(%{a: :old}))
    IO.inspect(MapOverwriteCase.run())

Result:

:kept_original
:ok

False positive warnings:

warning: the following clause will never match:

    :old ->

because it attempts to match on the result of:

    m.a

which has type:

    integer()

└─ iex:11: MapOverwriteCase.original/1

warning: incompatible types given to domain/1:

    domain(%{a: :old})

given types:

    %{a: :old}

but expected one of:

    %{..., a: integer()}

└─ iex:22: MapOverwriteCase.run/0

Note list and tuple does not seem to be affected by similar bugs.
This is a distinct bug from #15509 and #15688

Expected behavior

The type system should require the key to exist in the original map but without constraining its value type to the replacement

Activity

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

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