Skip to content

Update all named keys at once for non-finite atom keys - #16025

Merged
josevalim merged 3 commits into
mainfrom
map-update-widen-atom-keys
Oct 28, 2026
Merged

josevalim merged 3 commits into
mainfrom
map-update-widen-atom-keys

Conversation

@josevalim

@josevalim josevalim commented Oct 26, 2026 •

Copy link
Copy Markdown
Member

When the key may be any atom outside of a finite set (atom(), dynamic(), atom() and not :a), map updates computed one alternative per named key. Chained updates then multiplied the number of map lines by the number of named keys on every step.

Now each line updates all named keys and domains at once, where each of them may keep its old value or have the new one. For example, updating %{key1: binary(), key2: pid()} with an atom() key to integer():

# before
%{key1: binary(), key2: integer()} or %{key1: integer(), key2: pid()}

# after
%{key1: binary() or integer(), key2: pid() or integer()}

This is a supertype of the per-key union, as already done when a key spans several non-atom domains, and gives one line per line. Some details:

  • If the key must exist (Map.update!, Map.replace!) and there is a single candidate key, it is replaced instead of widened
  • The empty map is still excluded after an insertion, unless the map has a required key
  • A negative is kept only if, for every updated key and domain, it already contains the old values of the line
  • Finite atom keys, such as :a or :b, are still updated precisely

Benchmarks

Program #16023 This PR
Multi-clause function updating a map that excludes %URI{}, %Date{} and %{path: binary()} (whole file) 1.43s 0.26s
Chained Map.update!/Map.replace! with dynamic keys on a 12-field map (typecheck) 75ms 0.33ms
4 × Map.put(map, String.to_atom(...), ...) on a 10-key map (typecheck) 38ms 0.12ms

Compiling Elixir itself emits no new warnings, and absinthe_federation, credo, open_api_spex, spitfire and sql compile in the same time with the same warnings.

Assisted-by: Claude Code:claude-opus-5-5

Popping or putting a key across many map alternatives unioned the
results into a single growing BDD, one alternative at a time, which
is quadratic on the number of alternatives. They are now accumulated
in lists and unioned pairwise:

  * When splitting map lines in map_dnf_pop_key_static
  * When grouping lines by their new field in map_update_named_keys
  * When combining the results of each named key

Pairwise unions use opt_map_union, so alternatives that differ in a
single field are still merged.

Typechecking chained Map.update!/Map.replace! with dynamic keys on a
12-field map goes from 408ms to 75ms, chained Map.put with
String.to_atom keys from 128ms to 38ms, and a multi-clause function
updating a map that excludes %URI{}, %Date{} and %{path: binary()}
from 5.6s to 1.4s.

Assisted-by: Claude Code:claude-opus-5-5
When the key may be any atom outside of a finite set, such as atom()
or dynamic(), map updates computed one alternative per named key.
Chained updates then multiplied the number of map lines by the number
of named keys on every step.

Now each line updates all named keys and domains at once, where each
of them may keep its old value or have the new one. This is a supertype
of the per-key union, as already done for non-atom domains, and gives
one line per line. If the key must exist and there is a single
candidate, it is replaced instead. Finite atom keys are still updated
precisely.

Assisted-by: Claude Code:claude-opus-5-5
@josevalim
josevalim requested a review from gldubc October 26, 2026 19:29
  * Forced constant updates, such as Map.put/3, remove the key from
    every map and put it back, computing old values only when the
    return type is required. map_put/3 and map_put_key/3 now go through
    map_update, removing the separate put implementation
  * Updates across several domains use map_update_widen/5 with no
    named keys, excluding the empty map whenever the key is present
  * New fields without negatives are computed by map_update_field/2,
    as constants are callbacks that ignore the old value

Assisted-by: Claude Code:claude-opus-5-5
@josevalim
josevalim changed the base branch from map-update-pairwise-union to main October 27, 2026 12:08
@josevalim josevalim closed this Oct 27, 2026
@josevalim josevalim reopened this Oct 27, 2026
@josevalim
josevalim merged commit c81e88f into main Oct 28, 2026
36 of 37 checks passed
@josevalim

Copy link
Copy Markdown
Member Author

💚 💙 💜 💛 ❤️

@josevalim
josevalim deleted the map-update-widen-atom-keys branch October 28, 2026 14:26
josevalim added a commit that referenced this pull request Oct 28, 2026
#16025 updates all named keys at once when the key may be any atom
outside of a finite set. With that change, updating the partitioned
input in "checks callback inputs across equivalent map partitions"
gives the same type as updating the original input, so the test now
asserts that both results are equal instead of hard-coding the old
per-key approximation.
@bedohack

bedohack commented Nov 2, 2026

Copy link
Copy Markdown

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

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants