Repository navigation
Possible false-positive type warning with Map.update/4 for a struct field #15687
Description
Activity
Just to make sure this is not the same issue as #15625, I also replaced all `String.t()` keys in the reproducer with atoms. The outcome appears to be the same.
The warning remained overall the same.
So unless I am misunderstanding something, this does not appear to be specifically related to string-key inference.defmodule Reproducer do defstruct users: %{}, groups: %{} @type t :: %__MODULE__{ users: %{atom() => map()}, groups: %{atom() => map()} } @field %{ user: :users, group: :groups } @spec put(t(), :user | :group, atom(), map()) :: t() def put(%__MODULE__{} = state, kind, id, %{} = value) when is_map_key(@field, kind) and is_atom(id) do state |> Map.update(@field[kind], %{id => value}, &Map.put(&1, id, value)) end @spec put2(t(), :user | :group, atom(), map()) :: t() def put2(%__MODULE__{} = state, kind, id, %{} = value) when kind in [:user, :group] and is_atom(id) do state |> Map.update(@field[kind], %{id => value}, &Map.put(&1, id, value)) end @spec put_user_no_warning(t(), atom(), map()) :: t() def put_user_no_warning(%__MODULE__{} = state, id, %{} = value) when is_atom(id) do state |> Map.update(:users, %{id => value}, &Map.put(&1, id, value)) end @spec put_user_non_struct_field(t(), atom(), map()) :: t() def put_user_non_struct_field(%__MODULE__{} = state, id, %{} = value) when is_atom(id) do state |> Map.update(:user, %{id => value}, &Map.put(&1, id, value)) end end
% rm -rf Elixir.Reproducer.beam; elixirc reproducer.ex warning: incompatible types given on function call within Map.update/4: Map.update(state, %{group: :groups, user: :users}[kind], %{id => value}, &Map.put(&1, id, value)) given types: dynamic(Reproducer) but function has type: (map() -> dynamic(map())) where "id" was given the types: # type: dynamic(atom()) # from: reproducer.ex:17:40 id # type: atom() # from: reproducer.ex:18:41 is_atom(id) where "kind" was given the type: # type: dynamic() # from: reproducer.ex:17:34 kind where "state" was given the type: # type: dynamic(%Reproducer{}) # from: reproducer.ex:17:25 %Reproducer{} = state where "value" was given the type: # type: dynamic(map()) # from: reproducer.ex:17:48 %{} = value type warning found at: │ 20 │ |> Map.update(@field[kind], %{id => value}, &Map.put(&1, id, value)) │ ~ │ └─ reproducer.ex:20:12: Reproducer.put/4 warning: incompatible types given on function call within Map.update/4: Map.update(state, %{group: :groups, user: :users}[kind], %{id => value}, &Map.put(&1, id, value)) given types: dynamic(Reproducer) but function has type: (map() -> dynamic(map())) where "id" was given the types: # type: dynamic(atom()) # from: reproducer.ex:24:41 id # type: atom() # from: reproducer.ex:25:40 is_atom(id) where "kind" was given the types: # type: dynamic(:group or :user) # from: reproducer.ex:24:35 kind # type: :group or :user # from: reproducer.ex:25 kind in [:user, :group] where "state" was given the type: # type: dynamic(%Reproducer{}) # from: reproducer.ex:24:26 %Reproducer{} = state where "value" was given the type: # type: dynamic(map()) # from: reproducer.ex:24:49 %{} = value type warning found at: │ 27 │ |> Map.update(@field[kind], %{id => value}, &Map.put(&1, id, value)) │ ~ │ └─ reproducer.ex:27:12: Reproducer.put2/4
It seems to be a bug in that we are passing the type of the first argument to the callback instead of the type of its values. Thank you!
Reacted by Anderson Bravalheri- changed the title
[-]Possible false-positive type warning with `Map.update/4` for a struct field.[/-][+]Possible false-positive type warning with `Map.update/4` for a struct field[/+]on Jul 28, 2026 Hi. I reproduced this in main after clean rebuild with otp 29. I was studying how type information propagtes when an external discriminator is translated into a struct before calling Map.update/4. A small validation showed:
- @field[kind] is inferred as dynamic();
- Map.fetch!(@field, kind) is inferred as
:groups | :usersand produces no warning; - a map heterogeneous reproduces the problem through and unrelated atom_key value, so the behaviour is not exclusive to struct;
- a deliberately incompatible callback is still correctly reported;
The access expression loses key precision causing the callback analysis in Map.update/4 to consider values from unrelated atom keys including
__struct__I would be happy to add regression tests and investigate the fix =D
is this available for contribution?Please do not send a PR as we are revisiting some of the theory to fully address it correctly.
Reacted by Patric
Existing issue
I did notice that issue #15625 seems a bit similar, but it was pointed out there that the inference does not work with string keys, which is not exactly the case here.
Elixir and Erlang/OTP versions
Operating system
Ubuntu 24.04.4 LTS [Windows 11 + WSL 2.7.11.0 (Kernel version: 6.18.33.2-2)]
Current behavior
First of all, thank you for all the work on the recent compiler improvements and incremental type system. The new features are very useful (and helpful to catch bugs).
I was working on a private project and I noticed what to me looks like a surprising false positive in the form of a warning.
The original code comes from a much larger codebase and looks quite different (and the call to
Map.updateis in the middle of a much larger pipeline), but I have tried to simplify it down to the smallest example that still triggers the same warning:# reproducer.ex% rm -rf Elixir.Reproducer.beam % elixirc reproducer.ex warning: incompatible types given on function call within Map.update/4: Map.update(state, %{group: :groups, user: :users}[kind], %{id => value}, &Map.put(&1, id, value)) given types: dynamic(Reproducer) but function has type: (map() -> dynamic(map())) where "id" was given the types: # type: binary() # from: reproducer.ex:17:40 id # type: binary() # from: reproducer.ex:18:41 is_binary(id) where "kind" was given the type: # type: dynamic() # from: reproducer.ex:17:34 kind where "state" was given the type: # type: dynamic(%Reproducer{}) # from: reproducer.ex:17:25 %Reproducer{} = state where "value" was given the type: # type: dynamic(map()) # from: reproducer.ex:17:48 %{} = value type warning found at: │ 20 │ |> Map.update(@field[kind], %{id => value}, &Map.put(&1, id, value)) │ ~ │ └─ reproducer.ex:20:12: Reproducer.put/4 warning: incompatible types given on function call within Map.update/4: Map.update(state, %{group: :groups, user: :users}[kind], %{id => value}, &Map.put(&1, id, value)) given types: dynamic(Reproducer) but function has type: (map() -> dynamic(map())) where "id" was given the types: # type: binary() # from: reproducer.ex:24:41 id # type: binary() # from: reproducer.ex:25:40 is_binary(id) where "kind" was given the types: # type: dynamic(:group or :user) # from: reproducer.ex:24:35 kind # type: :group or :user # from: reproducer.ex:25 kind in [:user, :group] where "state" was given the type: # type: dynamic(%Reproducer{}) # from: reproducer.ex:24:26 %Reproducer{} = state where "value" was given the type: # type: dynamic(map()) # from: reproducer.ex:24:49 %{} = value type warning found at: │ 27 │ |> Map.update(@field[kind], %{id => value}, &Map.put(&1, id, value)) │ ~ │ └─ reproducer.ex:27:12: Reproducer.put2/4Warnings are emitted for:
In both cases, the compiler seems to be able to correctly infer the type of the argument
kindas:group | :user(either via the directinclause or viais_map_keywith the module attribute).Yet the warning is still emitted, and it was not immediately clear to me what type mismatch the diagnostic is pointing to. That is why I have also included in the reproducer 2 other functions:
Which are very similar to
putandput2but use a literal atom instead of a lookup through@field.So it seems that somehow the error may be related to the typing of the last
fnargument passed toMap.update/4?Interestingly
put_user_non_struct_fielduses a field that doesn't belong in the struct (uservsusers), but there is no warning produced (I am not claiming there should be one, but it is interesting to see that in this case the warning is not triggered for the same kind offnargument passed toMap.update/4).What is surprising to me is that I cannot identify anything obviously wrong with the example. The compiler appears to infer most of the relevant information correctly during compilation, yet the final Map.update/4 still triggers a warning. To my eyes, it feels as if enough information is available to establish type correctness, but some of it may be getting lost before the callback argument is analysed.
Would this be considered:
I fully appreciate that this kind of static/type analysis is necessarily conservative, and I do not expect it to infer every valid program. False positives maybe inevitable in a flexible language.
That said, is there currently a way to suppress a specific compiler warning locally without disabling the warnings all together? (they are very useful in general and it would be a pity to loose some overall static analysis info).
Thank you again for all the work on the compiler and type system. Even if this ends up being an expected limitation, I would appreciate guidance on the preferred approach for handling this class of warning.
Expected behavior
Either no warning (similar behaviour to
put_user_no_warning/3), if this is considered a false positive, or a mechanism to suppress this specific warning when the programmer intentionally relies on a safe behaviour that is not currently inferred by the type analysis.