defmodule Module.Types.Descr do
  @moduledoc false

  # The descr contains a set-theoretic implementation of types.
  # Types are represented as maps of non-overlapping unions.
  # A bitmap is used to represent non-divisible types. All other
  # types require specific data structures.

  # Vocabulary:
  #
  # * DNF - disjunctive normal form which is a pair of unions and negations.
  #   In the case of maps, we augment each pair with the open/closed tag.

  import Bitwise

  @bit_binary 1 <<< 0
  @bit_empty_list 1 <<< 1
  @bit_integer 1 <<< 2
  @bit_float 1 <<< 3
  @bit_pid 1 <<< 4
  @bit_port 1 <<< 5
  @bit_reference 1 <<< 6
  @bit_fun 1 <<< 7
  @bit_top (1 <<< 8) - 1
  @bit_number @bit_integer ||| @bit_float

  @atom_top {:negation, :sets.new(version: 2)}
  @map_top [{:open, %{}, []}]
  @non_empty_list_top [{:term, :term, []}]
  @tuple_top [{:open, [], []}]
  @map_empty [{:closed, %{}, []}]

  @none %{}
  @empty_list %{bitmap: @bit_empty_list}
  @not_non_empty_list %{bitmap: @bit_top, atom: @atom_top, tuple: @tuple_top, map: @map_top}
  @term %{
    bitmap: @bit_top,
    atom: @atom_top,
    tuple: @tuple_top,
    map: @map_top,
    list: @non_empty_list_top
  }

  @empty_intersection [0, @none]
  @empty_difference [0, []]

  # Type definitions

  defguard is_descr(descr) when is_map(descr) or descr == :term

  defp descr_key?(:term, _key), do: true
  defp descr_key?(descr, key), do: is_map_key(descr, key)

  def dynamic(), do: %{dynamic: :term}
  def none(), do: @none
  def term(), do: :term

  @compile {:inline, unfold: 1}
  defp unfold(:term), do: unfolded_term()
  defp unfold(other), do: other
  defp unfolded_term, do: @term

  def atom(as), do: %{atom: atom_new(as)}
  def atom(), do: %{atom: @atom_top}
  def binary(), do: %{bitmap: @bit_binary}
  def closed_map(pairs), do: map_descr(:closed, pairs)
  def empty_list(), do: %{bitmap: @bit_empty_list}
  def empty_map(), do: %{map: @map_empty}
  def integer(), do: %{bitmap: @bit_integer}
  def float(), do: %{bitmap: @bit_float}
  def fun(), do: %{bitmap: @bit_fun}
  def list(type, tail \\ @empty_list), do: list_descr(type, tail, true)
  def non_empty_list(type, tail \\ @empty_list), do: list_descr(type, tail, false)
  def open_map(), do: %{map: @map_top}
  def open_map(pairs), do: map_descr(:open, pairs)
  def open_tuple(elements, _fallback \\ term()), do: tuple_descr(:open, elements)
  def pid(), do: %{bitmap: @bit_pid}
  def port(), do: %{bitmap: @bit_port}
  def reference(), do: %{bitmap: @bit_reference}
  def tuple(), do: %{tuple: @tuple_top}
  def tuple(elements), do: tuple_descr(:closed, elements)

  @boolset :sets.from_list([true, false], version: 2)
  def boolean(), do: %{atom: {:union, @boolset}}

  ## Optional

  # `not_set()` is a special base type that represents an not_set field in a map.
  # E.g., `%{a: integer(), b: not_set(), ...}` represents a map with an integer
  # field `a` and an not_set field `b`, and possibly other fields.
  #
  # The `if_set()` modifier is syntactic sugar for specifying the key as a union
  # of the key type and `not_set()`. For example, `%{:foo => if_set(integer())}`
  # is equivalent to `%{:foo => integer() or not_set()}`.
  #
  # `not_set()` has no meaning outside of map types.

  @not_set %{optional: 1}
  @term_or_optional Map.put(@term, :optional, 1)
  @term_or_dynamic_optional Map.put(@term, :dynamic, %{optional: 1})

  def not_set(), do: @not_set
  def if_set(:term), do: term_or_optional()
  def if_set(type), do: Map.put(type, :optional, 1)
  defp term_or_optional(), do: @term_or_optional

  @compile {:inline,
            keep_optional: 1, remove_optional: 1, remove_optional_static: 1, optional_to_term: 1}
  defp keep_optional(descr) do
    case descr do
      %{dynamic: %{optional: 1}} -> %{dynamic: %{optional: 1}}
      %{optional: 1} -> %{optional: 1}
      _ -> %{}
    end
  end

  defp remove_optional(descr) do
    case descr do
      %{dynamic: %{optional: _} = dynamic} when map_size(dynamic) == 1 ->
        Map.delete(descr, :dynamic)

      %{dynamic: %{optional: _} = dynamic} ->
        %{descr | dynamic: Map.delete(dynamic, :optional)}

      _ ->
        remove_optional_static(descr)
    end
  end

  defp remove_optional_static(descr) do
    case descr do
      %{optional: _} -> Map.delete(descr, :optional)
      descr -> descr
    end
  end

  defp optional_to_term(descr) do
    case descr do
      %{dynamic: %{optional: _}} -> @term_or_dynamic_optional
      %{optional: _} -> term_or_optional()
      _ -> :term
    end
  end

  defp pop_optional_static(:term), do: {false, :term}

  defp pop_optional_static(type) do
    case :maps.take(:optional, type) do
      :error -> {false, type}
      {1, type} -> {true, type}
    end
  end

  ## Set operations

  @doc """
  Returns true if the type has a gradual part.
  """
  def gradual?(:term), do: false
  def gradual?(descr), do: is_map_key(descr, :dynamic)

  @doc """
  Returns true if the type only has a gradual part.
  """
  def only_gradual?(%{dynamic: _} = descr), do: map_size(descr) == 1
  def only_gradual?(_), do: false

  @doc """
  Make a whole type dynamic.

  It is an optimized version of `intersection(dynamic(), type)`.
  """
  @compile {:inline, dynamic: 1}
  def dynamic(descr) do
    case descr do
      %{dynamic: dynamic} -> %{dynamic: dynamic}
      _ -> %{dynamic: descr}
    end
  end

  @compile {:inline, maybe_union: 2}
  defp maybe_union(nil, _fun), do: nil
  defp maybe_union(descr, fun), do: union(descr, fun.())

  @doc """
  Computes the union of two descrs.
  """
  def union(:term, other), do: optional_to_term(other)
  def union(other, :term), do: optional_to_term(other)
  def union(none, other) when none == @none, do: other
  def union(other, none) when none == @none, do: other

  def union(left, right) do
    left = unfold(left)
    right = unfold(right)
    is_gradual_left = gradual?(left)
    is_gradual_right = gradual?(right)

    cond do
      is_gradual_left and not is_gradual_right ->
        right_with_dynamic = Map.put(right, :dynamic, right)
        union_static(left, right_with_dynamic)

      is_gradual_right and not is_gradual_left ->
        left_with_dynamic = Map.put(left, :dynamic, left)
        union_static(left_with_dynamic, right)

      true ->
        union_static(left, right)
    end
  end

  @compile {:inline, union_static: 2}
  defp union_static(left, right) do
    symmetrical_merge(left, right, &union/3)
  end

  defp union(:atom, v1, v2), do: atom_union(v1, v2)
  defp union(:bitmap, v1, v2), do: v1 ||| v2
  defp union(:dynamic, v1, v2), do: dynamic_union(v1, v2)
  defp union(:list, v1, v2), do: list_union(v1, v2)
  defp union(:map, v1, v2), do: map_union(v1, v2)
  defp union(:optional, 1, 1), do: 1
  defp union(:tuple, v1, v2), do: tuple_union(v1, v2)

  @doc """
  Computes the intersection of two descrs.
  """
  def intersection(:term, other), do: remove_optional(other)
  def intersection(other, :term), do: remove_optional(other)
  def intersection(%{dynamic: :term}, other), do: dynamic(remove_optional(other))
  def intersection(other, %{dynamic: :term}), do: dynamic(remove_optional(other))

  def intersection(left, right) do
    left = unfold(left)
    right = unfold(right)
    is_gradual_left = gradual?(left)
    is_gradual_right = gradual?(right)

    cond do
      is_gradual_left and not is_gradual_right ->
        right_with_dynamic = Map.put(right, :dynamic, right)
        intersection_static(left, right_with_dynamic)

      is_gradual_right and not is_gradual_left ->
        left_with_dynamic = Map.put(left, :dynamic, left)
        intersection_static(left_with_dynamic, right)

      true ->
        intersection_static(left, right)
    end
  end

  @compile {:inline, intersection_static: 2}
  defp intersection_static(left, right) do
    symmetrical_intersection(left, right, &intersection/3)
  end

  # Returning 0 from the callback is taken as none() for that subtype.
  defp intersection(:atom, v1, v2), do: atom_intersection(v1, v2)
  defp intersection(:bitmap, v1, v2), do: v1 &&& v2
  defp intersection(:dynamic, v1, v2), do: dynamic_intersection(v1, v2)
  defp intersection(:list, v1, v2), do: list_intersection(v1, v2)
  defp intersection(:map, v1, v2), do: map_intersection(v1, v2)
  defp intersection(:optional, 1, 1), do: 1
  defp intersection(:tuple, v1, v2), do: tuple_intersection(v1, v2)

  @doc """
  Computes the difference between two types.
  """
  def difference(left, :term), do: keep_optional(left)

  def difference(left, right) do
    left = unfold(left)
    right = unfold(right)

    if gradual?(left) or gradual?(right) do
      {left_dynamic, left_static} = Map.pop(left, :dynamic, left)
      {right_dynamic, right_static} = Map.pop(right, :dynamic, right)
      dynamic_part = difference_static(left_dynamic, right_static)

      if empty?(dynamic_part),
        do: none(),
        else: Map.put(difference_static(left_static, right_dynamic), :dynamic, dynamic_part)
    else
      difference_static(left, right)
    end
  end

  # For static types, the difference is component-wise.
  defp difference_static(left, :term), do: keep_optional(left)

  defp difference_static(left, right) do
    iterator_difference_static(:maps.next(:maps.iterator(unfold(right))), unfold(left))
  end

  defp iterator_difference_static({key, v2, iterator}, map) do
    acc =
      case map do
        %{^key => v1} ->
          value = difference(key, v1, v2)

          if value in @empty_difference do
            Map.delete(map, key)
          else
            %{map | key => value}
          end

        %{} ->
          map
      end

    iterator_difference_static(:maps.next(iterator), acc)
  end

  defp iterator_difference_static(:none, map), do: map

  # Returning 0 from the callback is taken as none() for that subtype.
  defp difference(:atom, v1, v2), do: atom_difference(v1, v2)
  defp difference(:bitmap, v1, v2), do: v1 - (v1 &&& v2)
  defp difference(:dynamic, v1, v2), do: dynamic_difference(v1, v2)
  defp difference(:list, v1, v2), do: list_difference(v1, v2)
  defp difference(:map, v1, v2), do: map_difference(v1, v2)
  defp difference(:optional, 1, 1), do: 0
  defp difference(:tuple, v1, v2), do: tuple_difference(v1, v2)

  @doc """
  Compute the negation of a type.
  """
  def negation(:term), do: none()
  def negation(%{} = descr), do: difference(unfolded_term(), descr)

  @doc """
  Check if a type is empty.

  For gradual types, check that the upper bound (the dynamic part) is empty.
  Stop as soon as one non-empty component is found. Simpler components
  (bitmap, atom) are checked first for speed since, if they are present,
  the type is non-empty as we normalize then during construction.
  """
  def empty?(:term), do: false

  def empty?(%{} = descr) do
    case Map.get(descr, :dynamic, descr) do
      :term ->
        false

      value when value == @none ->
        true

      descr ->
        not Map.has_key?(descr, :atom) and
          not Map.has_key?(descr, :bitmap) and
          not Map.has_key?(descr, :optional) and
          (not Map.has_key?(descr, :map) or map_empty?(descr.map)) and
          (not Map.has_key?(descr, :list) or list_empty?(descr.list)) and
          (not Map.has_key?(descr, :tuple) or tuple_empty?(descr.tuple))
    end
  end

  # For atom, bitmap, and optional, if the key is present,
  # then they are not empty,
  defp empty_key?(:map, value), do: map_empty?(value)
  defp empty_key?(:list, value), do: list_empty?(value)
  defp empty_key?(:tuple, value), do: tuple_empty?(value)
  defp empty_key?(_, _value), do: false

  @doc """
  Converts a descr to its quoted representation.

  ## Options

    * `:collapse_structs` - do not show struct fields that match
      their default type
  """
  def to_quoted(descr, opts \\ []) do
    if term_type?(descr) do
      {:term, [], []}
    else
      # Dynamic always come first for visibility
      {dynamic, descr} =
        case :maps.take(:dynamic, descr) do
          :error -> {[], descr}
          {dynamic, descr} -> {to_quoted(:dynamic, dynamic, opts), descr}
        end

      # Merge empty list and list together if they both exist
      {extra, descr} =
        case descr do
          %{list: list, bitmap: bitmap} when (bitmap &&& @bit_empty_list) != 0 ->
            descr = descr |> Map.delete(:list) |> Map.replace!(:bitmap, bitmap - @bit_empty_list)

            case list_to_quoted(list, :list, opts) do
              [] -> {[{:empty_list, [], []}], descr}
              unions -> {unions, descr}
            end

          %{} ->
            {[], descr}
        end

      unions =
        dynamic ++
          Enum.sort(
            extra ++ Enum.flat_map(descr, fn {key, value} -> to_quoted(key, value, opts) end)
          )

      case unions do
        [] -> {:none, [], []}
        unions -> Enum.reduce(unions, &{:or, [], [&2, &1]})
      end
    end
  end

  defp to_quoted(:atom, val, _opts), do: atom_to_quoted(val)
  defp to_quoted(:bitmap, val, _opts), do: bitmap_to_quoted(val)
  defp to_quoted(:dynamic, descr, opts), do: dynamic_to_quoted(descr, opts)
  defp to_quoted(:map, dnf, opts), do: map_to_quoted(dnf, opts)
  defp to_quoted(:list, dnf, opts), do: list_to_quoted(dnf, :non_empty_list, opts)
  defp to_quoted(:tuple, dnf, opts), do: tuple_to_quoted(dnf, opts)

  @doc """
  Converts a descr to its quoted string representation.
  """
  def to_quoted_string(descr, opts \\ []) do
    descr
    |> to_quoted(opts)
    |> Code.Formatter.to_algebra()
    |> Inspect.Algebra.format(98)
    |> IO.iodata_to_binary()
  end

  ## Type relations

  @doc """
  Check if a type is a subtype of another.

  If     `left  = (left_dyn  and dynamic()) or left_static`
  and    `right = (right_dyn and dynamic()) or right_static`

  then the gradual subtyping relation defined in Definition 6.5 page 125 of
  the thesis https://vlanvin.fr/papers/thesis.pdf is:

  `left <= right` if and only if
    - `left_static <= right_static`
    - `left_dyn <= right_dyn`

  Because of the dynamic/static invariant in the `descr`, subtyping can be
  simplified in several cases according to which type is gradual or not.
  """
  def subtype?(left, right) do
    left = unfold(left)
    right = unfold(right)
    is_grad_left = gradual?(left)
    is_grad_right = gradual?(right)

    cond do
      is_grad_left and not is_grad_right ->
        left_dynamic = Map.get(left, :dynamic)
        subtype_static?(left_dynamic, right)

      is_grad_right and not is_grad_left ->
        right_static = Map.delete(right, :dynamic)
        subtype_static?(left, right_static)

      true ->
        subtype_static?(left, right)
    end
  end

  defp subtype_static?(same, same), do: true
  defp subtype_static?(left, right), do: empty?(difference_static(left, right))

  @doc """
  Check if a type is equal to another.

  It is currently not optimized. Only to be used in tests.
  """
  def equal?(left, right) do
    left == right or (subtype?(left, right) and subtype?(right, left))
  end

  @doc """
  Check if two types are disjoint.

  This reimplements intersection/2 but aborts as it finds a disjoint part.
  """
  def disjoint?(:term, other), do: empty?(remove_optional(other))
  def disjoint?(other, :term), do: empty?(remove_optional(other))
  def disjoint?(%{dynamic: :term}, other), do: empty?(remove_optional(other))
  def disjoint?(other, %{dynamic: :term}), do: empty?(remove_optional(other))

  def disjoint?(left, right) do
    left = unfold(left)
    right = unfold(right)
    is_gradual_left = gradual?(left)
    is_gradual_right = gradual?(right)

    cond do
      is_gradual_left and not is_gradual_right ->
        right_with_dynamic = Map.put(right, :dynamic, right)
        not non_disjoint_intersection?(left, right_with_dynamic)

      is_gradual_right and not is_gradual_left ->
        left_with_dynamic = Map.put(left, :dynamic, left)
        not non_disjoint_intersection?(left_with_dynamic, right)

      true ->
        not non_disjoint_intersection?(left, right)
    end
  end

  @doc """
  Checks if a type is a compatible subtype of another.

  If `input_type` has a static part (i.e., values that are known to appear and
  need to be handled), then to be compatible it should be a subtype of the
  the dynamic part of `expected_type` (that is, the largest allowed type at
  runtime).

  If `input_type` is a dynamic type, then we check that the two can intersect
  at runtime, i.e. it is possible to get valid inputs at runtime.

  The function is used, in gradual mode, to type an operator that expects a given
  type. For instance, `+` expects `integer() or float()` inputs. Compatible inputs
  include `dynamic()`, `integer()`, but also `dynamic() and (integer() or atom())`.
  Incompatible subtypes include `integer() or list()`, `dynamic() and atom()`.
  """
  def compatible?(left, right) do
    {left_dynamic, left_static} =
      case left do
        :term -> {:term, :term}
        _ -> Map.pop(left, :dynamic, left)
      end

    right_dynamic =
      case right do
        %{dynamic: dynamic} -> dynamic
        _ -> right
      end

    if empty?(left_static) do
      not disjoint?(left_dynamic, right_dynamic)
    else
      subtype_static?(left_static, right_dynamic)
    end
  end

  @doc """
  Optimized version of `not empty?(term(), type)`.
  """
  def term_type?(:term), do: true
  def term_type?(descr), do: subtype_static?(unfolded_term(), Map.delete(descr, :dynamic))

  @doc """
  Optimized version of `not empty?(intersection(empty_list(), type))`.
  """
  def empty_list_type?(:term), do: true
  def empty_list_type?(%{dynamic: :term}), do: true

  def empty_list_type?(%{dynamic: %{bitmap: bitmap}}) when (bitmap &&& @bit_empty_list) != 0,
    do: true

  def empty_list_type?(%{bitmap: bitmap}) when (bitmap &&& @bit_empty_list) != 0, do: true
  def empty_list_type?(_), do: false

  @doc """
  Optimized version of `not empty?(intersection(binary(), type))`.
  """
  def binary_type?(:term), do: true
  def binary_type?(%{dynamic: :term}), do: true
  def binary_type?(%{dynamic: %{bitmap: bitmap}}) when (bitmap &&& @bit_binary) != 0, do: true
  def binary_type?(%{bitmap: bitmap}) when (bitmap &&& @bit_binary) != 0, do: true
  def binary_type?(_), do: false

  @doc """
  Optimized version of `not empty?(intersection(integer(), type))`.
  """
  def integer_type?(:term), do: true
  def integer_type?(%{dynamic: :term}), do: true
  def integer_type?(%{dynamic: %{bitmap: bitmap}}) when (bitmap &&& @bit_integer) != 0, do: true
  def integer_type?(%{bitmap: bitmap}) when (bitmap &&& @bit_integer) != 0, do: true
  def integer_type?(_), do: false

  @doc """
  Optimized version of `not empty?(intersection(float(), type))`.
  """
  def float_type?(:term), do: true
  def float_type?(%{dynamic: :term}), do: true
  def float_type?(%{dynamic: %{bitmap: bitmap}}) when (bitmap &&& @bit_float) != 0, do: true
  def float_type?(%{bitmap: bitmap}) when (bitmap &&& @bit_float) != 0, do: true
  def float_type?(_), do: false

  @doc """
  Optimized version of `not empty?(intersection(integer() or float(), type))`.
  """
  def number_type?(:term), do: true
  def number_type?(%{dynamic: :term}), do: true
  def number_type?(%{dynamic: %{bitmap: bitmap}}) when (bitmap &&& @bit_number) != 0, do: true
  def number_type?(%{bitmap: bitmap}) when (bitmap &&& @bit_number) != 0, do: true
  def number_type?(_), do: false

  @doc """
  Optimized version of `not empty?(intersection(atom(), type))`.
  """
  def atom_type?(:term), do: true
  def atom_type?(%{dynamic: :term}), do: true
  def atom_type?(%{dynamic: %{atom: _}}), do: true
  def atom_type?(%{atom: _}), do: true
  def atom_type?(_), do: false

  ## Bitmaps

  defp bitmap_to_quoted(val) do
    pairs =
      [
        binary: @bit_binary,
        empty_list: @bit_empty_list,
        integer: @bit_integer,
        float: @bit_float,
        pid: @bit_pid,
        port: @bit_port,
        reference: @bit_reference,
        fun: @bit_fun
      ]

    for {type, mask} <- pairs,
        (mask &&& val) !== 0,
        do: {type, [], []}
  end

  ## Funs

  @doc """
  Checks there is a function type (and only functions) with said arity.
  """
  def fun_fetch(:term, _arity), do: :error

  def fun_fetch(%{} = descr, _arity) do
    {static_or_dynamic, static} = Map.pop(descr, :dynamic, descr)

    if fun_only?(static) do
      case static_or_dynamic do
        :term -> :ok
        %{bitmap: bitmap} when (bitmap &&& @bit_fun) != 0 -> :ok
        %{} -> :error
      end
    else
      :error
    end
  end

  defp fun_only?(descr), do: empty?(difference(descr, fun()))

  ## Atoms

  # The atom component of a type consists of pairs `{tag, set}` where `set` is a
  # set of atoms.
  # If `tag = :union` the pair represents the union of the atoms in `set`.
  # Else, if `tag = :negation`, it represents every atom except those in `set`.
  #
  # Example:
  #   - `{:union, :sets.from_list([:a, :b])}` represents type `:a or :b`
  #   - `{:negation, :sets.from_list([:c, :d])}` represents type `atom() \ (:c or :d)
  #
  # `{:negation, :sets.new()}` is the `atom()` top type, as it is the difference
  # of `atom()` with an empty list.
  #
  # `{:union, :sets.new()}` is the empty type for atoms, as it is the union of
  # an empty list of atoms. It is simplified to `0` in set operations, and the key
  # is removed from the map.

  @false_or_nil_atoms [
    :sets.from_list([false, nil], version: 2),
    :sets.from_list([nil], version: 2),
    :sets.from_list([false], version: 2)
  ]

  @doc """
  Compute the truthness of an element.

  It is either :undefined, :always_true, or :always_false.
  """
  def truthness(:term), do: :undefined

  def truthness(%{} = descr) do
    descr = Map.get(descr, :dynamic, descr)

    case descr do
      :term ->
        :undefined

      %{atom: {:union, set}}
      when map_size(descr) == 1 and set in @false_or_nil_atoms ->
        :always_false

      %{atom: {:union, set}}
      when map_size(descr) == 1 and not is_map_key(set, false) and not is_map_key(set, nil) ->
        :always_true

      %{atom: {:negation, %{nil => _, false => _}}} ->
        :always_true

      %{atom: _} ->
        :undefined

      _ when map_size(descr) == 0 ->
        :undefined

      _ ->
        :always_true
    end
  end

  @doc """
  Optimized version of `not empty?(intersection(atom([atom]), type))`.
  """
  def atom_type?(:term, _atom), do: true

  def atom_type?(%{} = descr, atom) do
    case Map.get(descr, :dynamic, descr) do
      :term -> true
      %{atom: {:union, set}} -> :sets.is_element(atom, set)
      %{atom: {:negation, set}} -> not :sets.is_element(atom, set)
      %{} -> false
    end
  end

  @doc """
  Returns a set of all known atoms.

  Returns `{:finite or :infinite, known_set}` if it is an atom,
  `:error` otherwise. Notice `known_set` may be empty in infinite
  cases, due to negations.
  """
  def atom_fetch(:term), do: :error

  def atom_fetch(%{} = descr) do
    {static_or_dynamic, static} = Map.pop(descr, :dynamic, descr)

    if atom_only?(static) do
      case static_or_dynamic do
        :term -> {:infinite, []}
        %{atom: {:union, set}} -> {:finite, :sets.to_list(set)}
        %{atom: {:negation, _}} -> {:infinite, []}
        %{} -> :error
      end
    else
      :error
    end
  end

  defp atom_only?(descr), do: empty?(Map.delete(descr, :atom))
  defp atom_new(as) when is_list(as), do: {:union, :sets.from_list(as, version: 2)}

  defp atom_intersection({tag1, s1}, {tag2, s2}) do
    {tag, s} =
      case {tag1, tag2} do
        {:union, :union} -> {:union, :sets.intersection(s1, s2)}
        {:negation, :negation} -> {:negation, :sets.union(s1, s2)}
        {:union, :negation} -> {:union, :sets.subtract(s1, s2)}
        {:negation, :union} -> {:union, :sets.subtract(s2, s1)}
      end

    if tag == :union and :sets.size(s) == 0, do: 0, else: {tag, s}
  end

  defp atom_union({:union, s1}, {:union, s2}), do: {:union, :sets.union(s1, s2)}
  defp atom_union({:negation, s1}, {:negation, s2}), do: {:negation, :sets.intersection(s1, s2)}
  defp atom_union({:union, s1}, {:negation, s2}), do: {:negation, :sets.subtract(s2, s1)}
  defp atom_union({:negation, s1}, {:union, s2}), do: {:negation, :sets.subtract(s1, s2)}

  defp atom_difference({tag1, s1}, {tag2, s2}) do
    {tag, s} =
      case {tag1, tag2} do
        {:union, :union} -> {:union, :sets.subtract(s1, s2)}
        {:negation, :negation} -> {:union, :sets.subtract(s2, s1)}
        {:union, :negation} -> {:union, :sets.intersection(s1, s2)}
        {:negation, :union} -> {:negation, :sets.union(s1, s2)}
      end

    if tag == :union and :sets.size(s) == 0, do: 0, else: {tag, s}
  end

  defp literal_to_quoted(lit) do
    if is_atom(lit) and Macro.classify_atom(lit) == :alias do
      segments =
        case Atom.to_string(lit) do
          "Elixir" ->
            [:"Elixir"]

          "Elixir." <> segments ->
            segments
            |> String.split(".")
            |> Enum.map(&String.to_atom/1)
        end

      {:__aliases__, [], segments}
    else
      {:__block__, [], [lit]}
    end
  end

  defp atom_to_quoted({:union, a}) do
    if :sets.is_subset(@boolset, a) do
      entries =
        :sets.subtract(a, @boolset)
        |> :sets.to_list()
        |> Enum.map(&literal_to_quoted/1)

      [{:boolean, [], []} | entries]
    else
      :sets.to_list(a)
      |> Enum.map(&literal_to_quoted/1)
    end
  end

  defp atom_to_quoted({:negation, a}) do
    if :sets.size(a) == 0 do
      {:atom, [], []}
    else
      atom_to_quoted({:union, a})
      |> Kernel.then(&{:and, [], [{:atom, [], []}, {:not, [], &1}]})
    end
    |> List.wrap()
  end

  ## List

  # Represents both list and improper list simultaneously using a pair {list_type, last_type}.
  #
  # For proper lists, the last_type is empty_list().
  # In general, list(term(), term()) is interpreted as {term(), term()}
  # and not non_empty_list(term(), term()).
  #
  # Note: A type being none() is handled separately.
  defp list_descr(list_type, last_type, empty?) do
    {list_dynamic?, list_type} = list_pop_dynamic(list_type)
    {last_dynamic?, last_type} = list_pop_dynamic(last_type)

    list_part =
      if last_type == :term do
        list_new(term(), term())
      else
        case :maps.take(:list, last_type) do
          :error ->
            list_new(list_type, last_type)

          {dnf, last_type} ->
            {list_type, last_type} =
              Enum.reduce(dnf, {list_type, last_type}, fn {head, tail, _}, {acc_head, acc_tail} ->
                {union(head, acc_head), union(tail, acc_tail)}
              end)

            list_new(list_type, last_type)
        end
      end

    list_descr =
      if empty?, do: %{list: list_part, bitmap: @bit_empty_list}, else: %{list: list_part}

    case list_dynamic? or last_dynamic? do
      true -> %{dynamic: list_descr}
      false -> list_descr
    end
  end

  defp list_new(list_type, last_type) do
    [{list_type, last_type, []}]
  end

  defp list_pop_dynamic(:term), do: {false, :term}

  defp list_pop_dynamic(descr) do
    case :maps.take(:dynamic, descr) do
      :error -> {false, descr}
      {dynamic, _} -> {true, dynamic}
    end
  end

  defp list_tail_unfold(:term), do: @not_non_empty_list
  defp list_tail_unfold(other), do: Map.delete(other, :list)

  defp list_union(dnf1, dnf2), do: dnf1 ++ (dnf2 -- dnf1)

  defp list_intersection(dnf1, dnf2) do
    for {list_type1, last_type1, negs1} <- dnf1,
        {list_type2, last_type2, negs2} <- dnf2,
        reduce: [] do
      acc ->
        inter = intersection(list_type1, list_type2)
        last = intersection(last_type1, last_type2)

        if empty?(inter) or empty?(last) do
          acc
        else
          [{inter, last, negs1 ++ negs2} | acc]
        end
    end
    |> case do
      [] -> 0
      dnf -> dnf
    end
  end

  # Computes the difference between two DNF (Disjunctive Normal Form) list types.
  # It progressively subtracts each type in dnf2 from all types in dnf1.
  # The algorithm handles three cases:
  # 1. Disjoint types: keeps the original type from dnf1
  # 2. Subtype relationship:
  #    a) If dnf2 type is a supertype, keeps only the negations
  #    b) If only the last type differs, subtracts it
  # 3. Base case: adds dnf2 type to negations of dnf1 type
  # The result may be larger than the initial dnf1, which is maintained in the accumulator.
  defp list_difference(dnf1, dnf2) do
    Enum.reduce(dnf2, dnf1, fn {t2, last2, negs2}, acc_dnf1 ->
      last2 = list_tail_unfold(last2)

      Enum.flat_map(acc_dnf1, fn {t1, last1, negs1} ->
        last1 = list_tail_unfold(last1)

        i = intersection(t1, t2)
        l = intersection(last1, last2)

        new_negs =
          Enum.reduce(negs2, [], fn {nt, nlast}, nacc ->
            t = intersection(t1, nt)
            last = intersection(last1, nlast)
            if empty?(t) or empty?(last), do: nacc, else: [{t, last, negs1} | nacc]
          end)

        cond do
          empty?(i) or empty?(l) -> [{t1, last1, negs1}]
          subtype?(t1, t2) and subtype?(last1, last2) -> new_negs
          subtype?(t1, t2) -> [{t1, difference(last1, last2), negs1} | new_negs]
          true -> [{t1, last1, [{t2, last2} | negs1]} | new_negs]
        end
      end)
    end)
  end

  defp list_empty?(dnf) do
    Enum.all?(dnf, fn {list_type, last_type, negs} ->
      last_type = list_tail_unfold(last_type)

      empty?(list_type) or empty?(last_type) or
        Enum.reduce_while(negs, last_type, fn {neg_type, neg_last}, acc_last_type ->
          if subtype?(list_type, neg_type) and subtype?(acc_last_type, neg_last) do
            {:halt, nil}
          else
            {:cont, difference(acc_last_type, neg_last)}
          end
        end)
        |> is_nil()
    end)
  end

  defp non_empty_list_only?(descr), do: empty?(Map.delete(descr, :list))

  @doc """
  Returns the head of a list.

  Errors on an empty list.
  On a non empty list of integers, it returns the first integer.
  On a non empty list of integers, with an atom head element, it returns the atom.
  """
  def list_hd(:term), do: :badnonemptylist

  def list_hd(%{} = descr) do
    case :maps.take(:dynamic, descr) do
      :error ->
        static_value = list_hd_static(descr)

        if non_empty_list_only?(descr) and not empty?(static_value) do
          {false, static_value}
        else
          :badnonemptylist
        end

      {dynamic, static} ->
        dynamic_value = list_hd_static(dynamic)

        if non_empty_list_only?(static) and not empty?(dynamic_value) do
          {true, union(dynamic(dynamic_value), list_hd_static(static))}
        else
          :badnonemptylist
        end
    end
  end

  defp list_hd_static(:term), do: :term

  defp list_hd_static(descr) do
    case descr do
      %{list: [{type, _last, _negs}]} ->
        type

      %{list: dnf} ->
        Enum.reduce(dnf, none(), fn {type, _, _}, acc -> union(type, acc) end)

      %{} ->
        none()
    end
  end

  @doc """
  Returns the tail of a list.

  Errors on a possibly empty list.
  On a non empty list of integers, it returns a (possibly empty) list of integers.
  On a non empty list of integers, with an atom tail element, it returns either an atom,
  or a (possibly empty) list of integers with an atom tail element.
  """
  def list_tl(:term), do: :badnonemptylist

  def list_tl(descr) do
    case :maps.take(:dynamic, descr) do
      :error ->
        static_value = list_tl_static(descr)

        if non_empty_list_only?(descr) and not empty?(static_value) do
          {false, static_value}
        else
          :badnonemptylist
        end

      {dynamic, static} ->
        dynamic_value = list_tl_static(dynamic)

        if non_empty_list_only?(static) and not empty?(dynamic_value) do
          {true, union(dynamic(dynamic_value), list_tl_static(static))}
        else
          :badnonemptylist
        end
    end
  end

  defp list_tl_static(:term), do: :term

  defp list_tl_static(descr) do
    case descr do
      %{list: dnf} ->
        Enum.reduce(dnf, %{list: dnf, bitmap: @bit_empty_list}, fn {_, last, _}, acc ->
          union(last, acc)
        end)

      %{bitmap: bitmap} when (bitmap &&& @bit_empty_list) != 0 ->
        empty_list()

      %{} ->
        none()
    end
  end

  defp list_to_quoted(dnf, name, opts) do
    dnf = list_normalize(dnf)

    for {list_type, last_type, negs} <- dnf, reduce: [] do
      acc ->
        arguments =
          if subtype?(last_type, @empty_list) do
            [to_quoted(list_type, opts)]
          else
            [to_quoted(list_type, opts), to_quoted(last_type, opts)]
          end

        if negs == [] do
          [{name, [], arguments} | acc]
        else
          negs
          |> Enum.map(fn {ty, lst} ->
            args =
              if subtype?(lst, @empty_list) do
                [to_quoted(ty, opts)]
              else
                [to_quoted(ty, opts), to_quoted(lst, opts)]
              end

            {name, [], args}
          end)
          |> Enum.reduce(&{:or, [], [&2, &1]})
          |> Kernel.then(
            &[
              {:and, [], [{name, [], arguments}, {:not, [], [&1]}]}
              | acc
            ]
          )
        end
    end
  end

  # Eliminate empty lists from the union, and redundant types (that are subtypes of others,
  # or that can be merged with others).
  defp list_normalize(dnf) do
    Enum.reduce(dnf, [], fn {lt, last, negs}, acc ->
      if list_literal_empty?(lt, last, negs),
        do: acc,
        else: add_to_list_normalize(acc, lt, last, negs)
    end)
  end

  defp list_literal_empty?(list_type, last_type, negations) do
    empty?(list_type) or empty?(last_type) or
      Enum.any?(negations, fn {neg_type, neg_last} ->
        subtype?(list_type, neg_type) and subtype?(last_type, neg_last)
      end)
  end

  # Inserts a list type into a list of non-subtype list types.
  # If the {list_type, last_type} is a subtype of an existing type, the negs
  # are added to that type.
  # If one list member is a subtype of {list_type, last_type}, it is replaced
  # and its negations are added to the new type.
  # If the type of elements are the same, the last types are merged.
  defp add_to_list_normalize([{t, l, n} | rest], list, last, negs) do
    cond do
      subtype?(list, t) and subtype?(last, l) -> [{t, l, n ++ negs} | rest]
      subtype?(t, list) and subtype?(l, last) -> [{list, last, n ++ negs} | rest]
      equal?(t, list) -> [{t, union(l, last), n ++ negs} | rest]
      true -> [{t, l, n} | add_to_list_normalize(rest, list, last, negs)]
    end
  end

  defp add_to_list_normalize([], list, last, negs), do: [{list, last, negs}]

  ## Dynamic
  #
  # A type with a dynamic component is a gradual type; without, it is a static
  # type. The dynamic component itself is a static type; hence, any gradual type
  # can be written using a pair of static types as the union:
  #
  # `type = (dynamic_component and dynamic()) or static_component`
  #
  # where the `static_component` is simply the rest of the `descr`, and `dynamic()`
  # is the base type that can represent any value at runtime. The dynamic and
  # static parts can be considered separately for a mixed-typed analysis. For
  # example, the type
  #
  # `type = (dynamic() and integer()) or boolean()`
  #
  # denotes an expression that evaluates to booleans or integers; however, there is
  # uncertainty about the source of these integers. In static mode, the
  # type-checker refuses to apply a function of type `boolean() -> boolean()` to
  # this argument (since the argument may turn out to be an integer()), but in
  # dynamic mode, it considers the type obtained by replacing `dynamic()` with
  # `none()` (that is, `boolean()`), accepts the application, but types it with a
  # type that contains `dynamic()`.
  #
  # When pattern matching on an expression of type `type`, the static part (here,
  # booleans) has to be handled exhaustively. In contrast, the dynamic part can
  # produce potential warnings in specific user-induced conditions, such as asking
  # for stronger enforcement of static types.
  #
  # During construction and through set operations, we maintain the invariant that
  # the dynamic component is a supertype of the static one, formally
  # `dynamic_component >= static_component`
  #
  # With this invariant, the dynamic component always represents every value that
  # a given gradual type can take at runtime, allowing us to simplify set operations,
  # compared, for example, to keeping only the extra dynamic type that can obtained
  # on top of the static type. Though, the latter may be used for printing purposes.
  #
  # There are two ways for a descr to represent a static type: either the
  # `:dynamic` field is not_set, or it contains a type equal to the static component
  # (that is, there are no extra dynamic values).

  defp dynamic_union(:term, other), do: optional_to_term(other)
  defp dynamic_union(other, :term), do: optional_to_term(other)

  defp dynamic_union(left, right),
    do: symmetrical_merge(unfold(left), unfold(right), &union/3)

  defp dynamic_intersection(:term, other), do: remove_optional_static(other)
  defp dynamic_intersection(other, :term), do: remove_optional_static(other)

  defp dynamic_intersection(left, right),
    do: symmetrical_intersection(unfold(left), unfold(right), &intersection/3)

  defp dynamic_difference(left, right) do
    case difference_static(left, right) do
      value when value == @none -> 0
      value -> value
    end
  end

  defp dynamic_to_quoted(descr, opts) do
    cond do
      term_type?(descr) ->
        [{:dynamic, [], []}]

      single = indivisible_bitmap(descr) ->
        [single]

      true ->
        case to_quoted(descr, opts) do
          {:none, _meta, []} = none -> [none]
          descr -> [{:dynamic, [], [descr]}]
        end
    end
  end

  defp indivisible_bitmap(descr) do
    with %{bitmap: bitmap} when map_size(descr) == 1 <- descr,
         [single] <- bitmap_to_quoted(bitmap) do
      single
    else
      _ -> nil
    end
  end

  ## Map
  #
  # Maps are in disjunctive normal form (DNF), that is, a list (union) of pairs
  # `{map_literal, negated_literals}` where `map_literal` is a map type literal
  # and `negated_literals` is a list of map type literals that are negated from it.
  # Each pair is augmented with the :open or :closed tag.
  #
  # For instance, the type `%{..., a: integer()} and not %{b: atom()}` can be represented
  # by the DNF containing one pair of shape:
  #
  #     {:open, %{a => integer()}, [{:closed, %{b => atom()}}]}
  #
  # The goal of keeping symbolic negations is to avoid distributing difference on
  # every member of a union which creates a lot of map literals in the union and
  # requires emptiness checks to avoid creating empty maps.
  #
  # For instance, the difference between `%{...}` and `%{a: atom(), b: integer()}`
  # is the union of `%{..., a: atom(), b: if_set(not integer())}` and
  # `%{..., a: if_set(not atom()), b: integer()}`. For maps with more keys,
  # each key in a negated literal may create a new union when eliminated.

  defp map_descr(tag, fields) do
    case map_descr_pairs(fields, [], false) do
      {fields, true} ->
        %{dynamic: %{map: map_new(tag, fields |> Enum.reverse() |> :maps.from_list())}}

      {_, false} ->
        %{map: map_new(tag, :maps.from_list(fields))}
    end
  end

  defp map_descr_pairs([{key, :term} | rest], acc, dynamic?) do
    map_descr_pairs(rest, [{key, :term} | acc], dynamic?)
  end

  defp map_descr_pairs([{key, value} | rest], acc, dynamic?) do
    case :maps.take(:dynamic, value) do
      :error -> map_descr_pairs(rest, [{key, value} | acc], dynamic?)
      {dynamic, _static} -> map_descr_pairs(rest, [{key, dynamic} | acc], true)
    end
  end

  defp map_descr_pairs([], acc, dynamic?) do
    {acc, dynamic?}
  end

  defp tag_to_type(:open), do: term_or_optional()
  defp tag_to_type(:closed), do: not_set()

  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_only?(descr), do: empty?(Map.delete(descr, :map))

  # Union is list concatenation
  defp map_union(dnf1, dnf2), do: dnf1 ++ (dnf2 -- dnf1)

  # Given two unions of maps, intersects each pair of maps.
  defp map_intersection(dnf1, dnf2) do
    for {tag1, pos1, negs1} <- dnf1,
        {tag2, pos2, negs2} <- dnf2,
        reduce: [] do
      acc ->
        try do
          {tag, fields} = map_literal_intersection(tag1, pos1, tag2, pos2)
          [{tag, fields, negs1 ++ negs2} | acc]
        catch
          :empty -> acc
        end
    end
    |> case do
      [] -> 0
      acc -> acc
    end
  end

  # Intersects two map literals; throws if their intersection is empty.
  # Both open: the result is open.
  defp map_literal_intersection(:open, map1, :open, map2) do
    new_fields =
      symmetrical_merge(map1, map2, fn _, type1, type2 ->
        non_empty_intersection!(type1, type2)
      end)

    {:open, new_fields}
  end

  # Both closed: the result is closed.
  defp map_literal_intersection(:closed, map1, :closed, map2) do
    new_fields =
      symmetrical_intersection(map1, map2, fn _, type1, type2 ->
        non_empty_intersection!(type1, type2)
      end)

    if map_size(new_fields) < map_size(map1) or map_size(new_fields) < map_size(map2) do
      throw(:empty)
    end

    {:closed, new_fields}
  end

  # Open and closed: result is closed, all fields from open should be in closed, except not_set ones.
  defp map_literal_intersection(:open, open, :closed, closed) do
    :maps.iterator(open) |> :maps.next() |> map_literal_intersection_loop(closed)
  end

  defp map_literal_intersection(:closed, closed, :open, open) do
    :maps.iterator(open) |> :maps.next() |> map_literal_intersection_loop(closed)
  end

  defp map_literal_intersection_loop(:none, acc), do: {:closed, acc}

  defp map_literal_intersection_loop({key, type1, iterator}, acc) do
    case acc do
      %{^key => type2} ->
        acc = %{acc | key => non_empty_intersection!(type1, type2)}
        :maps.next(iterator) |> map_literal_intersection_loop(acc)

      _ ->
        # If the key is marked as not_set in the open map, we can ignore it.
        if type1 == @not_set do
          :maps.next(iterator) |> map_literal_intersection_loop(acc)
        else
          throw(:empty)
        end
    end
  end

  defp non_empty_intersection!(type1, type2) do
    type = intersection(type1, type2)
    if empty?(type), do: throw(:empty), else: type
  end

  defp map_difference(dnf1, dnf2) do
    Enum.reduce(dnf2, dnf1, fn
      # Optimization: we are removing an open map with one field.
      {:open, fields2, []}, dnf1 when map_size(fields2) == 1 ->
        Enum.reduce(dnf1, [], fn {tag1, fields1, negs1}, acc ->
          {key, value, _rest} = :maps.next(:maps.iterator(fields2))
          t_diff = difference(Map.get(fields1, key, tag_to_type(tag1)), value)

          if empty?(t_diff) do
            acc
          else
            [{tag1, Map.put(fields1, key, t_diff), negs1} | acc]
          end
        end)

      {tag2, fields2, negs2}, dnf1 ->
        Enum.reduce(dnf1, [], fn {tag1, fields1, negs1}, acc ->
          acc = [{tag1, fields1, [{tag2, fields2} | negs1]} | acc]

          Enum.reduce(negs2, acc, fn {neg_tag2, neg_fields2}, acc ->
            try do
              {tag, fields} = map_literal_intersection(tag1, fields1, neg_tag2, neg_fields2)
              [{tag, fields, negs1} | acc]
            catch
              :empty -> acc
            end
          end)
        end)
    end)
    |> case do
      [] -> 0
      acc -> acc
    end
  end

  @doc """
  Fetches the type of the value returned by accessing `key` on `map`
  with the assumption that the descr is exclusively a map (or dynamic).

  It returns a two element tuple or `:error`. The first element says
  if the type is dynamically optional or not, the second element is
  the type. In static mode, optional keys are not allowed.
  """
  def map_fetch(:term, _key), do: :badmap

  def map_fetch(%{} = descr, key) when is_atom(key) do
    case :maps.take(:dynamic, descr) do
      :error ->
        if descr_key?(descr, :map) and map_only?(descr) do
          {static_optional?, static_type} = map_fetch_static(descr, key)

          if static_optional? or empty?(static_type) do
            :badkey
          else
            {false, static_type}
          end
        else
          :badmap
        end

      {dynamic, static} ->
        if descr_key?(dynamic, :map) and map_only?(static) do
          {dynamic_optional?, dynamic_type} = map_fetch_static(dynamic, key)
          {static_optional?, static_type} = map_fetch_static(static, key)

          if static_optional? or empty?(dynamic_type) do
            :badkey
          else
            {dynamic_optional?, union(dynamic(dynamic_type), static_type)}
          end
        else
          :badmap
        end
    end
  end

  # Optimization: if the key does not exist in the map, avoid building
  # if_set/not_set pairs and return the popped value directly.
  defp map_fetch_static(%{map: [{tag, fields, []}]}, key) when not is_map_key(fields, key) do
    case tag do
      :open -> {true, term()}
      :closed -> {true, none()}
    end
  end

  # Takes a map dnf and returns the union of types it can take for a given key.
  # If the key may be undefined, it will contain the `not_set()` type.
  defp map_fetch_static(%{map: dnf}, key) do
    dnf
    |> Enum.reduce(none(), fn
      # Optimization: if there are no negatives,
      # we can return the value directly.
      {_tag, %{^key => value}, []}, acc ->
        value |> union(acc)

      # Optimization: if there are no negatives
      # and the key does not exist, return the default one.
      {tag, %{}, []}, acc ->
        tag_to_type(tag) |> union(acc)

      {tag, fields, negs}, acc ->
        {fst, snd} = map_pop_key(tag, fields, key)

        case map_split_negative(negs, key) do
          :empty ->
            acc

          negative ->
            negative
            |> pair_make_disjoint()
            |> pair_eliminate_negations_fst(fst, snd)
            |> union(acc)
        end
    end)
    |> pop_optional_static()
  end

  defp map_fetch_static(%{}, _key), do: {false, none()}
  defp map_fetch_static(:term, _key), do: {true, term()}

  @doc """
  Fetches and puts a `key` of a given type, assuming that the descr is exclusively
  a map (or dynamic).
  """
  def map_fetch_and_put(:term, _key, _type), do: :badmap

  def map_fetch_and_put(descr, key, :term) when is_atom(key),
    do: map_fetch_and_put_shared(descr, key, :term)

  def map_fetch_and_put(descr, key, type) when is_atom(key) do
    case :maps.take(:dynamic, type) do
      :error -> map_fetch_and_put_shared(descr, key, type)
      {dynamic, _static} -> map_fetch_and_put_shared(dynamic(descr), key, dynamic)
    end
  end

  defp map_fetch_and_put_shared(descr, key, type) do
    map_take(descr, key, none(), &map_put_static(&1, key, type))
  end

  @doc """
  Puts a `key` of a given type, assuming that the descr is exclusively
  a map (or dynamic).
  """
  def map_put(:term, _key, _type), do: :badmap
  def map_put(descr, key, :term) when is_atom(key), do: map_put_shared(descr, key, :term)

  def map_put(descr, key, type) when is_atom(key) do
    case :maps.take(:dynamic, type) do
      :error -> map_put_shared(descr, key, type)
      {dynamic, _static} -> map_put_shared(dynamic(descr), key, dynamic)
    end
  end

  defp map_put_shared(descr, key, type) do
    with {nil, descr} <- map_take(descr, key, nil, &map_put_static(&1, key, type)) do
      {:ok, descr}
    end
  end

  # Directly inserts a key of a given type into every positive and negative map.
  defp map_put_static(%{map: dnf} = descr, key, type) do
    dnf =
      Enum.map(dnf, fn {tag, fields, negs} ->
        {tag, Map.put(fields, key, type),
         Enum.map(negs, fn {neg_tag, neg_fields} ->
           {neg_tag, Map.put(neg_fields, key, type)}
         end)}
      end)

    %{descr | map: dnf}
  end

  defp map_put_static(descr, _key, _type), do: descr

  @doc """
  Removes a key from a map type.
  """
  def map_delete(descr, key) do
    # We pass nil as the initial value so we can avoid computing the unions.
    with {nil, descr} <-
           map_take(descr, key, nil, &intersection_static(&1, open_map([{key, not_set()}]))) do
      {:ok, descr}
    end
  end

  @doc """
  Removes a key from a map type and return its type.

  ## Algorithm

  1. Split the map type based on the presence of the key.
  2. Take the second part of the split, which represents the union of all
     record types where the key has been explicitly removed.
  3. Intersect this with an open record type where the key is explicitly absent.
     This step eliminates the key from open record types where it was implicitly present.
  """
  def map_take(descr, key) do
    map_take(descr, key, none(), &intersection_static(&1, open_map([{key, not_set()}])))
  end

  @compile {:inline, map_take: 4}
  defp map_take(:term, _key, _initial, _updater), do: :badmap

  defp map_take(descr, key, initial, updater) when is_atom(key) do
    case :maps.take(:dynamic, descr) do
      :error ->
        if descr_key?(descr, :map) and map_only?(descr) do
          {optional?, taken, result} = map_take_static(descr, key, initial)

          cond do
            taken == nil -> {nil, updater.(result)}
            optional? or empty?(taken) -> :badkey
            true -> {taken, updater.(result)}
          end
        else
          :badmap
        end

      {dynamic, static} ->
        if descr_key?(dynamic, :map) and map_only?(static) do
          {_, dynamic_taken, dynamic_result} = map_take_static(dynamic, key, initial)
          {static_optional?, static_taken, static_result} = map_take_static(static, key, initial)
          result = union(dynamic(updater.(dynamic_result)), updater.(static_result))

          cond do
            static_taken == nil and dynamic_taken == nil -> {nil, result}
            static_optional? or empty?(dynamic_taken) -> :badkey
            true -> {union(dynamic(dynamic_taken), static_taken), result}
          end
        else
          :badmap
        end
    end
  end

  # Takes a static map type and removes a key from it.
  # This allows the key to be put or deleted later on.
  defp map_take_static(%{map: [{tag, fields, []}]} = descr, key, initial)
       when not is_map_key(fields, key) do
    case tag do
      :open -> {true, maybe_union(initial, fn -> term() end), descr}
      :closed -> {true, initial, descr}
    end
  end

  defp map_take_static(%{map: dnf}, key, initial) do
    {value, map} =
      Enum.reduce(dnf, {initial, none()}, fn
        # Optimization: if there are no negatives, we can directly remove the key.
        {tag, fields, []}, {value, map} ->
          {fst, snd} = map_pop_key(tag, fields, key)
          {maybe_union(value, fn -> fst end), union(map, snd)}

        {tag, fields, negs}, {value, map} ->
          {fst, snd} = map_pop_key(tag, fields, key)

          case map_split_negative(negs, key) do
            :empty ->
              {value, map}

            negative ->
              disjoint = pair_make_disjoint(negative)

              {maybe_union(value, fn -> pair_eliminate_negations_fst(disjoint, fst, snd) end),
               disjoint |> pair_eliminate_negations_snd(fst, snd) |> union(map)}
          end
      end)

    if value == nil do
      {false, value, map}
    else
      {optional?, value} = pop_optional_static(value)
      {optional?, value, map}
    end
  end

  # If there is no map part to this static type, there is nothing to delete.
  defp map_take_static(%{}, _key, initial), do: {false, initial, none()}

  defp map_take_static(:term, _key, initial) do
    {true, maybe_union(initial, fn -> term() end), open_map()}
  end

  # Short-circuits if it finds a non-empty map literal in the union.
  # Since the algorithm is recursive, we implement the short-circuiting
  # as throw/catch.
  defp map_empty?(dnf) do
    Enum.all?(dnf, fn {tag, pos, negs} -> map_empty?(tag, pos, negs) end)
  end

  defp map_empty?(_, pos, []), do: Enum.any?(Map.to_list(pos), fn {_, v} -> empty?(v) end)
  defp map_empty?(_, _, [{:open, neg_fields} | _]) when neg_fields == %{}, do: true
  defp map_empty?(:open, fs, [{:closed, _} | negs]), do: map_empty?(:open, fs, negs)

  defp map_empty?(tag, fields, [{neg_tag, neg_fields} | negs]) do
    (Enum.all?(neg_fields, fn {neg_key, neg_type} ->
       cond do
         # Keys that are present in the negative map, but not in the positive one
         is_map_key(fields, neg_key) ->
           true

         # The key is not shared between positive and negative maps,
         # if the negative type is optional, then there may be a value in common
         tag == :closed ->
           is_optional_static(neg_type)

         # There may be value in common
         tag == :open ->
           diff = difference(term_or_optional(), neg_type)
           empty?(diff) or map_empty?(tag, Map.put(fields, neg_key, diff), negs)
       end
     end) and
       Enum.all?(fields, fn {key, type} ->
         case neg_fields do
           %{^key => neg_type} ->
             diff = difference(type, neg_type)
             empty?(diff) or map_empty?(tag, Map.put(fields, key, diff), negs)

           %{} ->
             cond do
               neg_tag == :open ->
                 true

               neg_tag == :closed and not is_optional_static(type) ->
                 false

               true ->
                 # an absent key in a open negative map can be ignored
                 diff = difference(type, tag_to_type(neg_tag))
                 empty?(diff) or map_empty?(tag, Map.put(fields, key, diff), negs)
             end
         end
       end)) or map_empty?(tag, fields, negs)
  end

  defp map_pop_key(tag, fields, key) do
    case :maps.take(key, fields) do
      {value, fields} -> {value, %{map: map_new(tag, fields)}}
      :error -> {tag_to_type(tag), %{map: map_new(tag, fields)}}
    end
  end

  defp map_split_negative(negs, key) do
    Enum.reduce_while(negs, [], fn
      # A negation with an open map means the whole thing is empty.
      {:open, fields}, _acc when map_size(fields) == 0 -> {:halt, :empty}
      {tag, fields}, neg_acc -> {:cont, [map_pop_key(tag, fields, key) | neg_acc]}
    end)
  end

  # Use heuristics to normalize a map dnf for pretty printing.
  defp map_normalize(dnf) do
    dnf
    |> Enum.reject(&map_empty?([&1]))
    |> Enum.map(fn {tag, fields, negs} ->
      {fields, negs} =
        Enum.reduce(negs, {fields, []}, fn neg = {neg_tag, neg_fields}, {acc_fields, acc_negs} ->
          if map_empty_negation?(tag, acc_fields, neg) do
            {acc_fields, acc_negs}
          else
            case map_all_but_one?(tag, acc_fields, neg_tag, neg_fields) do
              {:one, diff_key} ->
                {Map.update!(acc_fields, diff_key, &difference(&1, neg_fields[diff_key])),
                 acc_negs}

              _ ->
                {acc_fields, [neg | acc_negs]}
            end
          end
        end)

      {tag, fields, negs}
    end)
    |> map_fusion()
  end

  # Given a dnf, fuse maps when possible
  # e.g. %{a: integer(), b: atom()} or %{a: float(), b: atom()} into %{a: number(), b: atom()}
  defp map_fusion(dnf) do
    # Steps:
    # 1. Group maps by tags and keys
    # 2. Try fusions for each group until no fusion is found
    # 3. Merge the groups back into a dnf
    {without_negs, with_negs} = Enum.split_with(dnf, fn {_tag, _fields, negs} -> negs == [] end)

    without_negs =
      without_negs
      |> Enum.group_by(fn {tag, fields, _} -> {tag, Map.keys(fields)} end)
      |> Enum.flat_map(fn {_, maps} -> map_non_negated_fuse(maps) end)

    without_negs ++ with_negs
  end

  defp map_non_negated_fuse(maps) do
    Enum.reduce(maps, [], fn map, acc ->
      case Enum.split_while(acc, &non_fusible_maps?(map, &1)) do
        {_, []} ->
          [map | acc]

        {others, [match | rest]} ->
          fused = map_non_negated_fuse_pair(map, match)
          others ++ [fused | rest]
      end
    end)
  end

  # Two maps are fusible if they differ in at most one element.
  defp non_fusible_maps?({_, fields1, []}, {_, fields2, []}) do
    Enum.count_until(fields1, fn {key, value} -> Map.fetch!(fields2, key) != value end, 2) > 1
  end

  defp map_non_negated_fuse_pair({tag, fields1, []}, {_, fields2, []}) do
    fields =
      symmetrical_merge(fields1, fields2, fn _k, v1, v2 ->
        if v1 == v2, do: v1, else: union(v1, v2)
      end)

    {tag, fields, []}
  end

  # If all fields are the same except one, we can optimize map difference.
  defp map_all_but_one?(tag1, fields1, tag2, fields2) do
    keys1 = Map.keys(fields1)
    keys2 = Map.keys(fields2)

    if {tag1, tag2} == {:open, :closed} or
         :sets.from_list(keys1, version: 2) != :sets.from_list(keys2, version: 2) do
      :no
    else
      Enum.count(keys1, fn key -> Map.get(fields1, key) != Map.get(fields2, key) end)
      |> case do
        1 -> {:one, Enum.find(keys1, &(Map.get(fields1, &1) != Map.get(fields2, &1)))}
        _ -> :no
      end
    end
  end

  # Adapted from `map_empty?` to remove useless negations.
  defp map_empty_negation?(tag, fields, {neg_tag, neg_fields}) do
    (tag == :closed and
       Enum.any?(neg_fields, fn {neg_key, neg_type} ->
         not is_map_key(fields, neg_key) and not is_optional_static(neg_type)
       end)) or
      (neg_tag == :closed and
         Enum.any?(fields, fn {key, type} ->
           not is_map_key(neg_fields, key) and not is_optional_static(type)
         end))
  end

  defp map_to_quoted(dnf, opts) do
    dnf
    |> map_normalize()
    |> Enum.map(&map_each_to_quoted(&1, opts))
  end

  defp map_each_to_quoted({tag, positive_map, negative_maps}, opts) do
    case negative_maps do
      [] ->
        map_literal_to_quoted({tag, positive_map}, opts)

      _ ->
        negative_maps
        |> Enum.map(&map_literal_to_quoted(&1, opts))
        |> Enum.reduce(&{:or, [], [&2, &1]})
        |> Kernel.then(
          &{:and, [], [map_literal_to_quoted({tag, positive_map}, opts), {:not, [], [&1]}]}
        )
    end
  end

  def map_literal_to_quoted({:closed, fields}, _opts) when map_size(fields) == 0 do
    {:empty_map, [], []}
  end

  def map_literal_to_quoted({tag, fields}, opts) do
    case tag do
      :closed ->
        with %{__struct__: struct_descr} <- fields,
             {_, [struct]} <- atom_fetch(struct_descr) do
          fields = Map.delete(fields, :__struct__)

          fields =
            with true <- Keyword.get(opts, :collapse_structs, false),
                 [_ | _] = info <- maybe_struct(struct),
                 true <- Enum.all?(info, &is_map_key(fields, &1.field)) do
              Enum.reduce(info, fields, fn %{field: field}, acc ->
                # TODO: This should consider the struct default value
                if Map.fetch!(acc, field) == term() do
                  Map.delete(acc, field)
                else
                  acc
                end
              end)
            else
              _ -> fields
            end

          {:%, [],
           [
             literal_to_quoted(struct),
             {:%{}, [], map_fields_to_quoted(tag, fields, opts)}
           ]}
        else
          _ -> {:%{}, [], map_fields_to_quoted(tag, fields, opts)}
        end

      :open ->
        {:%{}, [], [{:..., [], nil} | map_fields_to_quoted(tag, fields, opts)]}
    end
  end

  defp maybe_struct(struct) do
    try do
      struct.__info__(:struct)
    rescue
      _ -> nil
    end
  end

  defp map_fields_to_quoted(tag, map, opts) do
    sorted = Enum.sort(Map.to_list(map))
    keyword? = Inspect.List.keyword?(sorted)

    for {key, type} <- sorted,
        not (tag == :open and is_optional_static(type) and term_type?(type)) do
      key =
        if keyword? do
          {:__block__, [format: :keyword], [key]}
        else
          literal_to_quoted(key)
        end

      {optional?, type} = pop_optional_static(type)

      cond do
        not optional? -> {key, to_quoted(type, opts)}
        empty?(type) -> {key, {:not_set, [], []}}
        true -> {key, {:if_set, [], [to_quoted(type, opts)]}}
      end
    end
  end

  ## Tuple

  # Represents tuple types in two forms:
  # 1. Closed tuples: Fixed-length tuples with specific element types
  #    Example: {integer(), atom()}
  # 2. Open tuples: Variable-length tuples with a minimum set of element types
  #    Example: {atom(), boolean(), ...}
  #
  # Internal representation:
  # - Closed tuple: {:closed, [element_type, ...]}
  # - Open tuple:   {:open, [element_type, ...]}
  #
  # Examples:
  # - {integer(), atom()} is encoded as {:closed, [integer(), atom()]}
  # - {atom(), boolean(), ...} is encoded as {:open, [atom(), boolean()]}

  defp tuple_descr(tag, fields) do
    case tuple_descr(fields, [], false) do
      {fields, true} -> %{dynamic: %{tuple: tuple_new(tag, Enum.reverse(fields))}}
      {_, false} -> %{tuple: tuple_new(tag, fields)}
    end
  end

  defp tuple_descr([:term | rest], acc, dynamic?) do
    tuple_descr(rest, [:term | acc], dynamic?)
  end

  defp tuple_descr([value | rest], acc, dynamic?) do
    case :maps.take(:dynamic, value) do
      :error -> tuple_descr(rest, [value | acc], dynamic?)
      {dynamic, _static} -> tuple_descr(rest, [dynamic | acc], true)
    end
  end

  defp tuple_descr([], acc, dynamic?) do
    {acc, dynamic?}
  end

  defp tuple_new(tag, elements), do: [{tag, elements, []}]

  defp tuple_intersection(dnf1, dnf2) do
    for {tag1, elements1, negs1} <- dnf1,
        {tag2, elements2, negs2} <- dnf2,
        reduce: [] do
      acc ->
        case tuple_literal_intersection(tag1, elements1, tag2, elements2) do
          {tag, fields} -> [{tag, fields, negs1 ++ negs2} | acc]
          :empty -> acc
        end
    end
    |> case do
      [] -> 0
      acc -> acc
    end
  end

  defp tuple_literal_intersection(tag1, elements1, tag2, elements2) do
    n = length(elements1)
    m = length(elements2)

    cond do
      (tag1 == :closed and n < m) or (tag2 == :closed and n > m) ->
        :empty

      tag1 == :open and tag2 == :open ->
        try do
          {:open, zip_non_empty_intersection!(elements1, elements2, [])}
        catch
          :empty -> :empty
        end

      true ->
        try do
          {:closed, zip_non_empty_intersection!(elements1, elements2, [])}
        catch
          :empty -> :empty
        end
    end
  end

  # Intersects two lists of types, and _appends_ the extra elements to the result.
  defp zip_non_empty_intersection!([], types2, acc), do: Enum.reverse(acc, types2)
  defp zip_non_empty_intersection!(types1, [], acc), do: Enum.reverse(acc, types1)

  defp zip_non_empty_intersection!([type1 | rest1], [type2 | rest2], acc) do
    zip_non_empty_intersection!(rest1, rest2, [non_empty_intersection!(type1, type2) | acc])
  end

  defp tuple_difference(dnf1, dnf2) do
    Enum.reduce(dnf2, dnf1, fn {tag2, elements2, negs2}, dnf1 ->
      Enum.reduce(dnf1, [], fn {tag1, elements1, negs1}, acc ->
        # Prune negations that have no values in common
        acc =
          case tuple_literal_intersection(tag1, elements1, tag2, elements2) do
            :empty -> [{tag1, elements1, negs1}] ++ acc
            _ -> [{tag1, elements1, [{tag2, elements2} | negs1]}] ++ acc
          end

        Enum.reduce(negs2, acc, fn {neg_tag2, neg_elements2}, inner_acc ->
          case tuple_literal_intersection(tag1, elements1, neg_tag2, neg_elements2) do
            :empty -> inner_acc
            {tag, fields} -> [{tag, fields, negs1} | inner_acc]
          end
        end)
      end)
    end)
    |> case do
      [] -> 0
      acc -> acc
    end
  end

  # Removes duplicates in union, which should trickle to other operations.
  # This is a cheap optimization that relies on structural equality.
  defp tuple_union(left, right), do: left ++ (right -- left)

  defp tuple_to_quoted(dnf, opts) do
    dnf
    |> tuple_simplify()
    |> tuple_fusion()
    |> Enum.map(&tuple_each_to_quoted(&1, opts))
  end

  # Given a dnf of tuples, fuses the tuple unions when possible,
  # e.g. {integer(), atom()} or {float(), atom()} into {number(), atom()}
  # The negations of two fused tuples are just concatenated.
  defp tuple_fusion(dnf) do
    # Steps:
    # 1. Consider tuples without negations apart from those with
    # 2. Group tuples by size and tag
    # 3. Try fusions for each group until no fusion is found
    # 4. Merge the groups back into a dnf
    {without_negs, with_negs} = Enum.split_with(dnf, fn {_tag, _elems, negs} -> negs == [] end)

    without_negs =
      without_negs
      |> Enum.group_by(fn {tag, elems, _} -> {tag, length(elems)} end)
      |> Enum.flat_map(fn {_, tuples} -> tuple_non_negated_fuse(tuples) end)

    without_negs ++ with_negs
  end

  defp tuple_non_negated_fuse(tuples) do
    Enum.reduce(tuples, [], fn tuple, acc ->
      case Enum.split_while(acc, &non_fusible_tuples?(tuple, &1)) do
        {_, []} ->
          [tuple | acc]

        {others, [match | rest]} ->
          fused = tuple_non_negated_fuse_pair(tuple, match)
          others ++ [fused | rest]
      end
    end)
  end

  # Two tuples are fusible if they have no negations and differ in at most one element.
  defp non_fusible_tuples?({_, elems1, []}, {_, elems2, []}) do
    Enum.zip(elems1, elems2) |> Enum.count_until(fn {a, b} -> a != b end, 2) > 1
  end

  defp tuple_non_negated_fuse_pair({tag, elems1, []}, {_, elems2, []}) do
    fused_elements =
      Enum.zip_with(elems1, elems2, fn a, b -> if a == b, do: a, else: union(a, b) end)

    {tag, fused_elements, []}
  end

  defp tuple_each_to_quoted({tag, positive_tuple, negative_tuples}, opts) do
    case negative_tuples do
      [] ->
        tuple_literal_to_quoted({tag, positive_tuple}, opts)

      _ ->
        negative_tuples
        |> Enum.map(&tuple_literal_to_quoted(&1, opts))
        |> Enum.reduce(&{:or, [], [&2, &1]})
        |> Kernel.then(
          &{:and, [], [tuple_literal_to_quoted({tag, positive_tuple}, opts), {:not, [], [&1]}]}
        )
    end
  end

  defp tuple_literal_to_quoted({:closed, []}, _opts), do: {:{}, [], []}

  defp tuple_literal_to_quoted({tag, elements}, opts) do
    case tag do
      :closed -> {:{}, [], Enum.map(elements, &to_quoted(&1, opts))}
      :open -> {:{}, [], Enum.map(elements, &to_quoted(&1, opts)) ++ [{:..., [], nil}]}
    end
  end

  # Pads a list of elements with term().
  defp tuple_fill(elements, desired_length) do
    pad_length = desired_length - length(elements)

    if pad_length < 0 do
      raise ArgumentError, "tuple_fill: elements are longer than the desired length"
    else
      elements ++ List.duplicate(term(), pad_length)
    end
  end

  # Check if a tuple represented in DNF is empty
  defp tuple_empty?(dnf) do
    Enum.all?(dnf, fn {tag, pos, negs} -> tuple_empty?(tag, pos, negs) end)
  end

  # No negations, so not empty unless there's an empty type
  defp tuple_empty?(_, pos, []), do: Enum.any?(pos, &empty?/1)
  # Open empty negation makes it empty
  defp tuple_empty?(_, _, [{:open, []} | _]), do: true
  # Open positive can't be emptied by a single closed negative
  defp tuple_empty?(:open, pos, [{:closed, _}]), do: Enum.any?(pos, &empty?/1)

  defp tuple_empty?(tag, elements, [{neg_tag, neg_elements} | negs]) do
    n = length(elements)
    m = length(neg_elements)

    # Scenarios where the difference is guaranteed to be empty:
    # 1. When removing larger tuples from a fixed-size positive tuple
    # 2. When removing smaller tuples from larger tuples
    if (tag == :closed and n < m) or (neg_tag == :closed and n > m) do
      tuple_empty?(tag, elements, negs)
    else
      tuple_elements_empty?([], tag, elements, neg_elements, negs) and
        tuple_compatibility(n, m, tag, elements, neg_tag, negs)
    end
  end

  # Recursively check elements for emptiness
  defp tuple_elements_empty?(_, _, _, [], _), do: true

  defp tuple_elements_empty?(acc, tag, elements, [neg_type | neg_elements], negs) do
    # Handles the case where {tag, elements} is an open tuple, like {:open, []}
    {ty, elements} = List.pop_at(elements, 0, term())
    diff = difference(ty, neg_type)

    (empty?(diff) or tuple_empty?(tag, Enum.reverse(acc, [diff | elements]), negs)) and
      tuple_elements_empty?([ty | acc], tag, elements, neg_elements, negs)
  end

  # Determines if the set difference is empty when:
  # - Positive tuple: {tag, elements} of size n
  # - Negative tuple: open or closed tuples of size m
  defp tuple_compatibility(n, m, tag, elements, neg_tag, negs) do
    # The tuples to consider are all those of size n to m - 1, and if the negative tuple is
    # closed, we also need to consider tuples of size greater than m + 1.
    tag == :closed or
      (Enum.all?(n..(m - 1)//1, &tuple_empty?(:closed, tuple_fill(elements, &1), negs)) and
         (neg_tag == :open or tuple_empty?(:open, tuple_fill(elements, m + 1), negs)))
  end

  @doc """
  Fetches the type of the value returned by accessing `index` on `tuple`
  with the assumption that the descr is exclusively a tuple (or dynamic).

  Returns one of:

  - `{false, type}` if the element is always accessible and has the given `type`.
  - `{true, type}` if the element is dynamically optional and has the given `type`.
  - `:badindex` if the index is never accessible in the tuple type.
  - `:badtuple` if the descr is not a tuple type.

  ## Examples

      iex> tuple_fetch(tuple([integer(), atom()]), 0)
      {false, integer()}

      iex> tuple_fetch(union(tuple([integer()]), tuple([integer(), atom()])), 1)
      {true, atom()}

      iex> tuple_fetch(dynamic(), 0)
      {true, dynamic()}

      iex> tuple_fetch(integer(), 0)
      :badtuple

  """
  def tuple_fetch(_, index) when index < 0, do: :badindex
  def tuple_fetch(:term, _key), do: :badtuple

  def tuple_fetch(%{} = descr, key) when is_integer(key) do
    case :maps.take(:dynamic, descr) do
      :error ->
        if descr_key?(descr, :tuple) and tuple_only?(descr) do
          {static_optional?, static_type} = tuple_fetch_static(descr, key)

          # If I access a static tuple at a "open position", we have two options:
          #
          # 1. Do not allow the access and return :badindex,
          #    you must use dynamic for these cases (this is what we chose for maps)
          #
          # 2. Allow the access and return the static type
          #
          # The trouble with allowing the access is that it is a potential runtime
          # error not being caught by the type system.
          #
          # Furthermore, our choice here, needs to be consistent with elem/put_elem
          # when the index is the `integer()` type. If we choose to return `:badindex`,
          # then all elem/put_elem with an `integer()` and the tuple is not dynamic
          # should also be a static typing error. We chose to go with 1.
          if static_optional? or empty?(static_type) do
            :badindex
          else
            {false, static_type}
          end
        else
          :badtuple
        end

      {dynamic, static} ->
        if descr_key?(dynamic, :tuple) and tuple_only?(static) do
          {dynamic_optional?, dynamic_type} = tuple_fetch_static(dynamic, key)
          {static_optional?, static_type} = tuple_fetch_static(static, key)

          if empty?(dynamic_type) do
            :badindex
          else
            {static_optional? or dynamic_optional?, union(dynamic(dynamic_type), static_type)}
          end
        else
          :badtuple
        end
    end
  end

  defp tuple_only?(descr), do: empty?(Map.delete(descr, :tuple))

  defp tuple_fetch_static(descr, index) when is_integer(index) do
    case descr do
      :term ->
        {true, term()}

      %{tuple: tuple} ->
        tuple_get(tuple, index)
        |> pop_optional_static()

      %{} ->
        {false, none()}
    end
  end

  defp tuple_get(dnf, index) do
    Enum.reduce(dnf, none(), fn
      # Optimization: if there are no negatives, just return the type at that index.
      {tag, elements, []}, acc ->
        Enum.at(elements, index, tag_to_type(tag)) |> union(acc)

      {tag, elements, negs}, acc ->
        {fst, snd} = tuple_pop_index(tag, elements, index)

        case tuple_split_negative(negs, index) do
          :empty ->
            acc

          negative ->
            negative
            |> pair_make_disjoint()
            |> pair_eliminate_negations_fst(fst, snd)
            |> union(acc)
        end
    end)
  end

  def tuple_values(descr) do
    case :maps.take(:dynamic, descr) do
      :error ->
        if tuple_only?(descr) do
          process_tuples_values(Map.get(descr, :tuple, []))
        else
          :badtuple
        end

      {dynamic, static} ->
        if tuple_only?(static) and descr_key?(dynamic, :tuple) do
          dynamic(process_tuples_values(Map.get(dynamic, :tuple, [])))
          |> union(process_tuples_values(Map.get(static, :tuple, [])))
        else
          :badtuple
        end
    end
  end

  defp process_tuples_values(dnf) do
    Enum.reduce(dnf, none(), fn {tag, elements, negs}, acc ->
      union(tuple_values(tag, elements, negs), acc)
    end)
  end

  defp tuple_values(tag, elements, []) do
    cond do
      Enum.any?(elements, &empty?/1) -> none()
      tag == :open -> term()
      tag == :closed -> Enum.reduce(elements, none(), &union/2)
    end
  end

  defp tuple_values(_tag, _elements, [{:open, []} | _]), do: none()

  defp tuple_values(tag, elements, [{neg_tag, neg_elements} | negs]) do
    n = length(elements)
    m = length(neg_elements)

    if (tag == :closed and n < m) or (neg_tag == :closed and n > m) do
      tuple_values(tag, elements, negs)
    else
      # Those two functions eliminate the negations, transforming into
      # a union of tuples to compute their values.
      values_elements([], tag, elements, neg_elements, negs)
      |> union(values_size(n, m, tag, elements, neg_tag, negs))
    end
  end

  # This means that there are no more neg_elements to subtract -- end the recursion.
  defp values_elements(_acc, _tag, _elements, [], _), do: none()

  # Eliminates negations according to tuple content.
  # Subtracts each element of a negative tuple to build a new tuple with the difference.
  # Example: {number(), atom()} and not {float(), :foo} contains types {integer(), :foo}
  # as well as {float(), atom() and not :foo}
  # Same process as tuple_elements_empty?
  defp values_elements(acc, tag, elements, [neg_type | neg_elements], negs) do
    {ty, elements} = List.pop_at(elements, 0, term())
    diff = difference(ty, neg_type)

    if empty?(diff) do
      none()
    else
      tuple_values(tag, Enum.reverse(acc, [diff | elements]), negs)
    end
    |> union(values_elements([ty | acc], tag, elements, neg_elements, negs))
  end

  # Eliminates negations according to size
  # Example: {integer(), ...} and not {term(), term(), ...} contains {integer()}
  defp values_size(n, m, tag, elements, neg_tag, negs) do
    if tag == :closed do
      none()
    else
      n..(m - 1)//1
      |> Enum.map(&tuple_values(:closed, tuple_fill(elements, &1), negs))
      |> Enum.reduce(none(), &union/2)
      |> union(
        if neg_tag == :open do
          none()
        else
          tuple_values(tag, tuple_fill(elements, m + 1), negs)
        end
      )
    end
  end

  defp tuple_pop_index(tag, elements, index) do
    case List.pop_at(elements, index) do
      {nil, _} -> {tag_to_type(tag), %{tuple: [{tag, elements, []}]}}
      {type, rest} -> {type, %{tuple: [{tag, rest, []}]}}
    end
  end

  defp tuple_split_negative(negs, index) do
    Enum.reduce_while(negs, [], fn
      {:open, []}, _acc -> {:halt, :empty}
      {tag, elements}, acc -> {:cont, [tuple_pop_index(tag, elements, index) | acc]}
    end)
  end

  # Use heuristics to simplify a tuple dnf for pretty printing.
  defp tuple_simplify(dnf) do
    for {tag, elements, negs} <- dnf,
        not tuple_empty?([{tag, elements, negs}]) do
      n = length(elements)
      {tag, elements, Enum.reject(negs, &tuple_empty_negation?(tag, n, &1))}
    end
  end

  @doc """
  Delete an element from the tuple.

  It returns the same as `tuple_fetch/2`.
  """
  # Same as tuple_delete but checks if the index is out of range.
  def tuple_delete_at(:term, _key), do: :badtuple

  def tuple_delete_at(descr, index) when is_integer(index) and index >= 0 do
    case :maps.take(:dynamic, descr) do
      :error ->
        # Note: the empty type is not a valid input
        is_proper_tuple? = descr_key?(descr, :tuple) and tuple_only?(descr)
        is_proper_size? = tuple_of_size_at_least_static?(descr, index + 1)

        cond do
          is_proper_tuple? and is_proper_size? -> tuple_delete_static(descr, index)
          is_proper_tuple? -> :badindex
          true -> :badtuple
        end

      {dynamic, static} ->
        is_proper_tuple? = descr_key?(dynamic, :tuple) and tuple_only?(static)
        is_proper_size? = tuple_of_size_at_least_static?(static, index + 1)

        cond do
          is_proper_tuple? and is_proper_size? ->
            static_result = tuple_delete_static(static, index)

            # Prune for dynamic values make the intersection succeed
            dynamic_result =
              intersection(dynamic, tuple_of_size_at_least(index))
              |> tuple_delete_static(index)

            union(dynamic(dynamic_result), static_result)

          # Highlight the case where the issue is an index out of range from the tuple
          is_proper_tuple? ->
            :badindex

          true ->
            :badtuple
        end
    end
  end

  def tuple_delete_at(_, _), do: :badindex

  # Takes a static map type and removes an index from it.
  defp tuple_delete_static(%{tuple: dnf}, index) do
    Enum.reduce(dnf, none(), fn
      # Optimization: if there are no negatives, we can directly remove the element
      {tag, elements, []}, acc ->
        union(acc, %{tuple: tuple_new(tag, List.delete_at(elements, index))})

      {tag, elements, negs}, acc ->
        {fst, snd} = tuple_pop_index(tag, elements, index)

        case tuple_split_negative(negs, index) do
          :empty ->
            acc

          negative ->
            negative
            |> pair_make_disjoint()
            |> pair_eliminate_negations_snd(fst, snd)
            |> union(acc)
        end
    end)
  end

  # If there is no map part to this static type, there is nothing to delete.
  defp tuple_delete_static(_type, _key), do: none()

  @doc """
  Insert an element at the tuple.

  It returns the same as `tuple_fetch/2`. Notice, however, the range for indexes is inclusive.
  """
  def tuple_insert_at(:term, _key, _type), do: :badtuple

  def tuple_insert_at(descr, index, type) when is_integer(index) and index >= 0 do
    case :maps.take(:dynamic, unfold(type)) do
      :error -> tuple_insert_at_checked(descr, index, type)
      {dynamic, _static} -> dynamic(tuple_insert_at_checked(descr, index, dynamic))
    end
  end

  def tuple_insert_at(_, _, _), do: :badindex

  defp tuple_insert_at_checked(descr, index, type) do
    case :maps.take(:dynamic, descr) do
      :error ->
        # Note: the empty type is not a valid input
        is_proper_tuple? = descr_key?(descr, :tuple) and tuple_only?(descr)
        is_proper_size? = index == 0 or tuple_of_size_at_least_static?(descr, index)

        cond do
          is_proper_tuple? and is_proper_size? -> tuple_insert_static(descr, index, type)
          is_proper_tuple? -> :badindex
          true -> :badtuple
        end

      {dynamic, static} ->
        is_proper_tuple? = descr_key?(dynamic, :tuple) and tuple_only?(static)
        is_proper_size? = index == 0 or tuple_of_size_at_least_static?(static, index)

        cond do
          is_proper_tuple? and is_proper_size? ->
            static_result = tuple_insert_static(static, index, type)

            # Prune for dynamic values that make the intersection succeed
            dynamic_result =
              intersection(dynamic, tuple_of_size_at_least(index))
              |> tuple_insert_static(index, type)

            union(dynamic(dynamic_result), static_result)

          # Highlight the case where the issue is an index out of range from the tuple
          is_proper_tuple? ->
            :badindex

          true ->
            :badtuple
        end
    end
  end

  defp tuple_insert_static(descr, _, _) when descr == @none, do: none()

  defp tuple_insert_static(descr, index, type) do
    Map.update!(descr, :tuple, fn dnf ->
      Enum.map(dnf, fn {tag, elements, negs} ->
        {tag, List.insert_at(elements, index, type),
         Enum.map(negs, fn {neg_tag, neg_elements} ->
           {neg_tag, List.insert_at(neg_elements, index, type)}
         end)}
      end)
    end)
  end

  # Remove useless negations, which denote tuples of incompatible sizes.
  defp tuple_empty_negation?(tag, n, {neg_tag, neg_elements}) do
    m = length(neg_elements)
    (tag == :closed and n < m) or (neg_tag == :closed and n > m)
  end

  defp tuple_of_size_at_least(n) when is_integer(n) and n >= 0 do
    tuple_descr(:open, List.duplicate(term(), n))
  end

  defp tuple_of_size_at_least_static?(descr, index) do
    case descr do
      %{tuple: dnf} ->
        Enum.all?(dnf, fn
          {_, elements, []} -> length(elements) >= index
          entry -> subtype?(%{tuple: [entry]}, tuple_of_size_at_least(index))
        end)

      %{} ->
        true
    end
  end

  ## Pairs

  # To simplify disjunctive normal forms of e.g., map types, it is useful to
  # convert them into disjunctive normal forms of pairs of types, and define
  # normalization algorithms on pairs.
  #
  # The algorithms take a line, a list of pairs `{positive, negative}` where
  # `positive` is a list of literals and `negative` is a list of negated literals.
  # Positive pairs can all be intersected component-wise. Negative ones are
  # eliminated iteratively.

  # Eliminates negations from `{t, s} and not negative` where `negative` is a
  # union of pairs disjoint on their first component.
  #
  # Formula:
  #   {t, s} and not (union<i=1..n> {t_i, s_i})
  #       = union<i=1..n> {t and t_i, s and not s_i}
  #            or {t and not (union{i=1..n} t_i), s}
  #
  # This eliminates all top-level negations and produces a union of pairs that
  # are disjoint on their first component. The function `pair_eliminate_negations_fst`
  # is optimized to only keep the first component out of those pairs.
  defp pair_eliminate_negations_fst(negative, t, s) do
    {pair_union, diff_of_t_i} =
      Enum.reduce(negative, {none(), t}, fn {t_i, s_i}, {accu, diff_of_t_i} ->
        i = intersection(t, t_i)

        if empty?(i) do
          {accu, diff_of_t_i}
        else
          diff_of_t_i = difference(diff_of_t_i, t_i)
          s_diff = difference(s, s_i)

          if empty?(s_diff),
            do: {accu, diff_of_t_i},
            else: {union(i, accu), diff_of_t_i}
        end
      end)

    union(pair_union, diff_of_t_i)
  end

  # The formula above is symmetric with respect to the first and second components.
  # Hence the following also holds true:
  #
  #    {t, s} and not (union<i=1..n> {t_i, s_i})
  #           = union<i=1..n> {t and not t_i, s and s_i}
  #            or {t, s and not (union{i=1..n} s_i)}
  #
  # which is used to in the following function, optimized to keep the second component.
  defp pair_eliminate_negations_snd(negative, t, s) do
    {pair_union, diff_of_s_i} =
      Enum.reduce(negative, {none(), s}, fn {t_i, s_i}, {accu, diff_of_s_i} ->
        i = intersection(s, s_i)

        if empty?(i) do
          {accu, diff_of_s_i}
        else
          diff_of_s_i = difference(diff_of_s_i, s_i)
          t_diff = difference(t, t_i)

          if empty?(t_diff),
            do: {accu, diff_of_s_i},
            else: {union(i, accu), diff_of_s_i}
        end
      end)

    union(diff_of_s_i, pair_union)
  end

  # Makes a union of pairs into an equivalent union of disjoint pairs.
  #
  # Inserts a pair of types {fst, snd} into a list of pairs that are disjoint
  # on their first component. The invariant on `acc` is that its elements are
  # two-to-two disjoint with the first argument's `pairs`.
  #
  # To insert {fst, snd} into a disjoint pairs list, we go through the list to find
  # each pair whose first element has a non-empty intersection with `fst`. Then
  # we decompose {fst, snd} over each such pair to produce disjoint ones, and add
  # the decompositions into the accumulator.
  defp pair_make_disjoint(pairs) do
    Enum.reduce(pairs, [], fn {t1, t2}, acc -> add_pair_to_disjoint_list(acc, t1, t2, []) end)
  end

  defp add_pair_to_disjoint_list([], fst, snd, acc), do: [{fst, snd} | acc]

  defp add_pair_to_disjoint_list([{s1, s2} | pairs], fst, snd, acc) do
    x = intersection(fst, s1)

    if empty?(x) do
      add_pair_to_disjoint_list(pairs, fst, snd, [{s1, s2} | acc])
    else
      fst_diff = difference(fst, s1)
      s1_diff = difference(s1, fst)
      empty_fst_diff = empty?(fst_diff)
      empty_s1_diff = empty?(s1_diff)

      cond do
        # if fst is a subtype of s1, the disjointness invariant ensures we can
        # add those two pairs and end the recursion
        empty_fst_diff and empty_s1_diff ->
          [{x, union(snd, s2)} | pairs ++ acc]

        empty_fst_diff ->
          [{s1_diff, s2}, {x, union(snd, s2)} | pairs ++ acc]

        empty_s1_diff ->
          add_pair_to_disjoint_list(pairs, fst_diff, snd, [{x, union(snd, s2)} | acc])

        true ->
          # case where, when comparing {fst, snd} and {s1, s2}, both (fst and not s1)
          # and (s1 and not fst) are non empty. that is, there is something in fst
          # that is not in s1, and something in s1 that is not in fst
          add_pair_to_disjoint_list(pairs, fst_diff, snd, [
            {s1_diff, s2},
            {x, union(snd, s2)} | acc
          ])
      end
    end
  end

  ## Map helpers

  defp symmetrical_merge(left, right, fun) do
    # Erlang maps:merge_with/3 has to preserve the order in combiner.
    # We don't care about the order, so we have a faster implementation.
    if map_size(left) > map_size(right) do
      iterator_merge(:maps.next(:maps.iterator(right)), left, fun)
    else
      iterator_merge(:maps.next(:maps.iterator(left)), right, fun)
    end
  end

  defp iterator_merge({key, v1, iterator}, map, fun) do
    acc =
      case map do
        %{^key => v2} -> %{map | key => fun.(key, v1, v2)}
        %{} -> Map.put(map, key, v1)
      end

    iterator_merge(:maps.next(iterator), acc, fun)
  end

  defp iterator_merge(:none, map, _fun), do: map

  defp symmetrical_intersection(left, right, fun) do
    # Erlang maps:intersect_with/3 has to preserve the order in combiner.
    # We don't care about the order, so we have a faster implementation.
    if map_size(left) > map_size(right) do
      iterator_intersection(:maps.next(:maps.iterator(right)), left, [], fun)
    else
      iterator_intersection(:maps.next(:maps.iterator(left)), right, [], fun)
    end
  end

  defp iterator_intersection({key, v1, iterator}, map, acc, fun) do
    acc =
      case map do
        %{^key => v2} ->
          value = fun.(key, v1, v2)

          if value in @empty_intersection do
            acc
          else
            [{key, value} | acc]
          end

        %{} ->
          acc
      end

    iterator_intersection(:maps.next(iterator), map, acc, fun)
  end

  defp iterator_intersection(:none, _map, acc, _fun), do: :maps.from_list(acc)

  defp non_disjoint_intersection?(left, right) do
    # Erlang maps:intersect_with/3 has to preserve the order in combiner.
    # We don't care about the order, so we have a faster implementation.
    if map_size(left) > map_size(right) do
      iterator_non_disjoint_intersection?(:maps.next(:maps.iterator(right)), left)
    else
      iterator_non_disjoint_intersection?(:maps.next(:maps.iterator(left)), right)
    end
  end

  defp iterator_non_disjoint_intersection?({key, v1, iterator}, map) do
    with %{^key => v2} <- map,
         value when value not in @empty_intersection <- intersection(key, v1, v2),
         false <- empty_key?(key, value) do
      true
    else
      _ -> iterator_non_disjoint_intersection?(:maps.next(iterator), map)
    end
  end

  defp iterator_non_disjoint_intersection?(:none, _map), do: false
end
