Skip to content

Domain keys in map #14478

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open

Domain keys in map #14478

wants to merge 20 commits into from

Conversation

gldubc
Copy link
Member

@gldubc gldubc commented May 9, 2025

This allows maps to encode domain keys, from paper https://www.irif.fr/~gc/papers/icfp23.pdf

For example, we can now express the type %{a: integer(), b: float(), atom() => tuple()} of maps that have keys :a and :b and any other atom key mapping to a tuple.

Those are created by adding {{:domain_key, :atom}, type} pairs to constructors open_map or closed_map.

This introduces new operators:

  • map_get, which computes the type obtained with expression map[key] where key is some expression of some type
  • map_refresh, which computes type the map obtained when doing Map.put(map, key, type) (not called map_put because the name is already used for adding a single key to a map. The difference here is that, key being a type, we are not sure which key is being added).

Both operators handle singleton atoms exactly. See the following tests:

 test "more complex map get over atoms" do
      map = closed_map([{:a, atom([:a])}, {:b, atom([:b])}, {{:domain_key, :atom}, pid()}])
      assert map_get(map, atom([:a, :b])) == {:ok_present, atom([:a, :b])}
      assert map_get(map, atom([:a, :c])) == {:ok, union(atom([:a]), pid() |> nil_or_type())}
      assert map_get(map, atom() |> difference(atom([:a, :b]))) == {:ok, pid() |> nil_or_type()}

      assert map_get(map, atom() |> difference(atom([:a]))) ==
               {:ok, union(atom([:b]), pid() |> nil_or_type())}
    end

  # Tricky cases with atoms:
  # We add one atom fields that maps to an integer, which is not :a. So we do not touch
  # :a, add integer to :b, and add a domain field.
      assert map_refresh(
               closed_map(a: pid(), b: pid()),
               atom() |> difference(atom([:a])),
               integer()
             ) ==
               {:ok,
                closed_map([
                  {:a, pid()},
                  {:b, union(pid(), integer())},
                  {{:domain_key, :atom}, integer()}
                ])}

Notes:

  • I have not tested enough the optimizations to know if they can be adapted to domains.
  • One operator missing is map_delete.
  • Function map_check_domain_keys(tag, neg_tag) is very long to treat all the different cases optimally. It could be shortened to a single loop on all the possible domains, with default values.

gldubc added 5 commits May 6, 2025 14:33
- Introduced tests for union, intersection, and difference operations involving domain key types.
- Validated subtype relationships and intersection results for maps with domain keys.
- Enhanced map fetch and delete functionalities to handle domain key types.
- Ensured correct behavior of dynamic types with domain keys in various scenarios.
…n_map_with_default to map_with_default and implement map_update functionality
@josevalim
Copy link
Member

I will lock this issue for now because of spam, will reopen once I have feedback. :(

@elixir-lang elixir-lang locked as spam and limited conversation to collaborators May 10, 2025
@josevalim josevalim added this to the v1.19 milestone May 11, 2025
@elixir-lang elixir-lang unlocked this conversation May 13, 2025
@josevalim josevalim modified the milestones: v1.19, v2.0 Jun 22, 2025

defguardp is_optional_static(map)
when is_map(map) and is_map_key(map, :optional)

defp map_new(tag, fields = %{}), do: [{tag, fields, []}]
defp map_new(tag, fields = %{}, domains = %{}), do: [{{tag, domains}, fields, []}]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should simply have the tag be either: :open, :closed, or domains. If you have a map as the tag, then it has domains key. If an open map, then we expand everything, as done in map_with_default.

Copy link
Member Author

@gldubc gldubc Jul 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it's possible to do so, the benefit of keeping them is just so we can represent {:open, domains} without putting a top type in all the undefined domains. But it does not give us much more.

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.

2 participants