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
% elixir -v
Erlang/OTP 29 [erts-17.0.3] [source] [64-bit] [smp:16:16] [ds:16:16:10] [async-threads:1] [jit:ns]
Elixir 1.20.2 (compiled with Erlang/OTP 27)
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.update is 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:
defmodule Reproducer do
defstruct users: %{},
groups: %{}
@type t :: %__MODULE__{
users: %{String.t() => map()},
groups: %{String.t() => map()}
}
@field %{
user: :users,
group: :groups
}
@spec put(t(), :user | :group, String.t(), map()) :: t()
def put(%__MODULE__{} = state, kind, id, %{} = value)
when is_map_key(@field, kind) and is_binary(id) do
state
|> Map.update(@field[kind], %{id => value}, &Map.put(&1, id, value))
end
@spec put2(t(), :user | :group, String.t(), map()) :: t()
def put2(%__MODULE__{} = state, kind, id, %{} = value)
when kind in [:user, :group] and is_binary(id) do
state
|> Map.update(@field[kind], %{id => value}, &Map.put(&1, id, value))
end
@spec put_user_no_warning(t(), String.t(), map()) :: t()
def put_user_no_warning(%__MODULE__{} = state, id, %{} = value)
when is_binary(id) do
state
|> Map.update(:users, %{id => value}, &Map.put(&1, id, value))
end
@spec put_user_non_struct_field(t(), String.t(), map()) :: t()
def put_user_non_struct_field(%__MODULE__{} = state, id, %{} = value)
when is_binary(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: 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/4
Warnings are emitted for:
In both cases, the compiler seems to be able to correctly infer the type of the argument kind as :group | :user (either via the direct in clause or via is_map_key with 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:
put_user_no_warning/3
put_user_non_struct_field/3
Which are very similar to put and put2 but 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 fn argument passed to Map.update/4?
Interestingly put_user_non_struct_field uses a field that doesn't belong in the struct (user vs users), 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 of fn argument passed to Map.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:
- A false-positive warning?
- An intentional limitation of the current analysis?
- Expected behaviour for this pattern?
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.
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.