Skip to content

Conversation

gldubc
Copy link
Member

@gldubc gldubc commented Aug 4, 2025

Changing the representation of maps, tuples to instead use trees (binary decision diagrams).

As we use more and more the difference operator in the type system (e.g. to compute fun_from_inferred_clauses(args_clauses) for inferred function types from patterns), the "DNF" representation started running into pathological cases and hog out time during emptiness checks, and printing.

The BDD is now used for set operations and subtyping, and translated (using previous logic and simplications) to DNF for printing and type operations like map fetch.

The BDD representation is common to maps, tuples, lists and functions. Some functions are generic (called bdd_difference, bdd_intersection, ...).

We also changes some algorithms:

  • tuple_eliminate_negation was not generating disjoint tuples. It now does, which greatly improves performance and printing quality.
    -tuple_empty? now uses the same logic, this improves performances.
  • pivot_overlapping_clauses algorithm was not generating disjoint tuples. It now does.
  • The computation of subtyping for functions and function application is too slow for large arrow intersections (>30). This adds a special case apply_disjoint, when the functions are disjoint and non-empty (as produced by fun_from_inferred_clauses, for instance), which is much faster.

gldubc added 9 commits July 24, 2025 16:56
- Updated the representation of non-empty lists and empty intersections/differences to use BDD structures.
- Introduced new functions for BDD operations including list_get, list_get_pos, and list_all? for better handling of BDD paths.
- Modified list normalization and difference functions to work with BDDs
@sabiwara
Copy link
Contributor

sabiwara commented Sep 2, 2025

Note: I haven't reviewed the code or investigated properly yet, but I tried the branch on a project and noticed:

  defguard is_doc(doc)
           when is_list(doc) or is_binary(doc) or doc == :doc_line or
                  (is_tuple(doc) and elem(doc, 0) in @docs)

Removing one element from @docs moves it down to ~2s, adding one element makes it ~45s.
It seems to be trying to type the whole AST tree of the guard macro.

@josevalim
Copy link
Member

I beleive the issue is the usage of comparison in the BDD. For example, maps only use comparisons up to 32 terms, because from them on, the linear time of each comparison becomes too high.

Copy link
Contributor

@sabiwara sabiwara left a comment

Choose a reason for hiding this comment

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

I noticed that to_quoted can now be very verbose for unions since it adds these and not - technically correct but not very useful - which I'm worried might harm readability:

union(
  open_map(a: number(), b: binary()),
  open_map(a: integer())
)

# 1.18
%{..., a: float() or integer()}

# this branch
%{..., a: float() or integer(), b: binary()} or
  (%{..., a: integer()} and not %{..., a: float() or integer(), b: binary()})

@gldubc
Copy link
Member Author

gldubc commented Sep 17, 2025

I noticed that to_quoted can now be very verbose for unions since it adds these and not - technically correct but not very useful - which I'm worried might harm readability:

union(
  open_map(a: number(), b: binary()),
  open_map(a: integer())
)

# 1.18
%{..., a: float() or integer()}

# this branch
%{..., a: float() or integer(), b: binary()} or
  (%{..., a: integer()} and not %{..., a: float() or integer(), b: binary()})

This is a drawback of the BDD representation. It is possible, I think, to get rid of those by using a more sophisticated normalization algorithm for printing (that I implemented in the past for a prototype, but not for this version).

@michallepicki
Copy link
Contributor

michallepicki commented Sep 26, 2025

After reporting #14790 I checked with this branch merged to main, and while Oban.Telemetry compilation terminates, our Phoenix projects compilation is extra slow at compiling and then verifying our router modules (I wasn't patient enough to wait for it to terminate on our projects). To reproduce:

mix phx.new thingy
cd thingy
mix phx.gen.html Users users name:string age:integer
mix phx.gen.html Users2 users2 name:string age:integer
# ... repeat a few times
mix phx.gen.html Users10 users10 name:string age:integer

and add this to the router under PageController, :home:

resources "/users", UserController
resources "/users2", User2Controller
resources "/users3", User3Controller
# ...
resources "/users10", User10Controller

Even with 3 resources it's extra slow. Both compilation and verifying takes a long time: when using sufficiently small number of routes and waiting long enough, after Compiling lib/thingy_web/router.ex (it's taking more than 10s), there will be another message about Verifying ThingyWeb.Router (it's taking more than 10s).

I am on Erlang 27.3.4.2

I hope this helps, I am sorry I was not able to narrow it down further 🙏🏻

@sabiwara
Copy link
Contributor

sabiwara commented Sep 27, 2025

Compiling lib/thingy_web/router.ex (it's taking more than 10s)

I can reproduce this. I seems it's stuck trying to type FooWeb.Router.call/2 which code AST is:

%{method: method, path_info: path_info, host: host} = conn = prepare(conn)
decoded = Enum.map(path_info, &URI.decode/1)

case __match_route__(decoded, method, host) do
  {metadata, prepare, pipeline, plug_opts} ->
    Phoenix.Router.__call__(conn, metadata, prepare, pipeline, plug_opts)

  :error ->
    :erlang.error(Phoenix.Router.NoRouteError.exception(conn: conn, router: FooWeb.Router))
end

I seems to spend a lot of time in map_line_empty?, here is a slow example taking more than half a second (need to make the fun public first: def pub_map_line_empty?(tag, fields, neg), do: map_line_empty?(tag, fields, neg):

 Module.Types.Descr.pub_map_line_empty?(
    :closed,
    %{
      log: %{atom: {:union, %{debug: []}}},
      plug: %{atom: {:union, %{FooWeb.EpsilonController => []}}},
      conn: %{atom: {:union, %{nil: []}}},
      path_params: %{map: {{:closed, %{}}, :bdd_top, :bdd_bot}},
      route: %{bitmap: 1},
      pipe_through: %{
        list:
          {{%{atom: {:union, %{api: [], require_authenticated_user: []}}}, %{bitmap: 2}},
           :bdd_top, :bdd_bot}
      },
      plug_opts: %{atom: {:union, %{create: []}}}
    },
    closed: %{
      log: %{atom: {:union, %{debug: []}}},
      plug: %{atom: {:union, %{FooWeb.OmegaController => []}}},
      conn: %{atom: {:union, %{nil: []}}},
      path_params: %{map: {{:closed, %{}}, :bdd_top, :bdd_bot}},
      route: %{bitmap: 1},
      pipe_through: %{
        list:
          {{%{
              atom: {:union, %{api: [], require_authenticated_user: []}}
            }, %{bitmap: 2}}, :bdd_top, :bdd_bot}
      },
      plug_opts: %{atom: {:union, %{new: []}}}
    },
    closed: %{
      log: %{atom: {:union, %{debug: []}}},
      plug: %{atom: {:union, %{FooWeb.OmegaController => []}}},
      conn: %{atom: {:union, %{nil: []}}},
      path_params: %{map: {{:closed, %{}}, :bdd_top, :bdd_bot}},
      route: %{bitmap: 1},
      pipe_through: %{
        list:
          {{%{
              atom: {:union, %{api: [], require_authenticated_user: []}}
            }, %{bitmap: 2}}, :bdd_top, :bdd_bot}
      },
      plug_opts: %{atom: {:union, %{index: []}}}
    },
    closed: %{
      log: %{atom: {:union, %{debug: []}}},
      plug: %{atom: {:union, %{FooWeb.OmegaController => []}}},
      conn: %{atom: {:union, %{nil: []}}},
      path_params: %{map: {{:closed, %{}}, :bdd_top, :bdd_bot}},
      route: %{bitmap: 1},
      pipe_through: %{
        list:
          {{%{
              atom: {:union, %{api: [], require_authenticated_user: []}}
            }, %{bitmap: 2}}, :bdd_top, :bdd_bot}
      },
      plug_opts: %{atom: {:union, %{create: []}}}
    },
    closed: %{
      log: %{atom: {:union, %{debug: []}}},
      plug: %{atom: {:union, %{FooWeb.PiController => []}}},
      conn: %{atom: {:union, %{nil: []}}},
      path_params: %{map: {{:closed, %{}}, :bdd_top, :bdd_bot}},
      route: %{bitmap: 1},
      pipe_through: %{
        list:
          {{%{
              atom: {:union, %{api: [], require_authenticated_user: []}}
            }, %{bitmap: 2}}, :bdd_top, :bdd_bot}
      },
      plug_opts: %{atom: {:union, %{new: []}}}
    },
    closed: %{
      log: %{atom: {:union, %{debug: []}}},
      plug: %{atom: {:union, %{FooWeb.PiController => []}}},
      conn: %{atom: {:union, %{nil: []}}},
      path_params: %{map: {{:closed, %{}}, :bdd_top, :bdd_bot}},
      route: %{bitmap: 1},
      pipe_through: %{
        list:
          {{%{
              atom: {:union, %{api: [], require_authenticated_user: []}}
            }, %{bitmap: 2}}, :bdd_top, :bdd_bot}
      },
      plug_opts: %{atom: {:union, %{index: []}}}
    },
    closed: %{
      log: %{atom: {:union, %{debug: []}}},
      plug: %{atom: {:union, %{FooWeb.PiController => []}}},
      conn: %{atom: {:union, %{nil: []}}},
      path_params: %{map: {{:closed, %{}}, :bdd_top, :bdd_bot}},
      route: %{bitmap: 1},
      pipe_through: %{
        list:
          {{%{
              atom: {:union, %{api: [], require_authenticated_user: []}}
            }, %{bitmap: 2}}, :bdd_top, :bdd_bot}
      },
      plug_opts: %{atom: {:union, %{create: []}}}
    },
    closed: %{
      log: %{atom: {:union, %{debug: []}}},
      plug: %{atom: {:union, %{FooWeb.DeltaController => []}}},
      conn: %{atom: {:union, %{nil: []}}},
      path_params: %{map: {{:closed, %{}}, :bdd_top, :bdd_bot}},
      route: %{bitmap: 1},
      pipe_through: %{
        list:
          {{%{
              atom: {:union, %{api: [], require_authenticated_user: []}}
            }, %{bitmap: 2}}, :bdd_top, :bdd_bot}
      },
      plug_opts: %{atom: {:union, %{new: []}}}
    },
    closed: %{
      log: %{atom: {:union, %{debug: []}}},
      plug: %{atom: {:union, %{FooWeb.DeltaController => []}}},
      conn: %{atom: {:union, %{nil: []}}},
      path_params: %{map: {{:closed, %{}}, :bdd_top, :bdd_bot}},
      route: %{bitmap: 1},
      pipe_through: %{
        list:
          {{%{
              atom: {:union, %{api: [], require_authenticated_user: []}}
            }, %{bitmap: 2}}, :bdd_top, :bdd_bot}
      },
      plug_opts: %{atom: {:union, %{index: []}}}
    },
    closed: %{
      log: %{atom: {:union, %{debug: []}}},
      plug: %{atom: {:union, %{FooWeb.DeltaController => []}}},
      conn: %{atom: {:union, %{nil: []}}},
      path_params: %{map: {{:closed, %{}}, :bdd_top, :bdd_bot}},
      route: %{bitmap: 1},
      pipe_through: %{
        list:
          {{%{
              atom: {:union, %{api: [], require_authenticated_user: []}}
            }, %{bitmap: 2}}, :bdd_top, :bdd_bot}
      },
      plug_opts: %{atom: {:union, %{create: []}}}
    },
    closed: %{
      log: %{atom: {:union, %{debug: []}}},
      plug: %{atom: {:union, %{FooWeb.GammaController => []}}},
      conn: %{atom: {:union, %{nil: []}}},
      path_params: %{map: {{:closed, %{}}, :bdd_top, :bdd_bot}},
      route: %{bitmap: 1},
      pipe_through: %{
        list:
          {{%{
              atom: {:union, %{api: [], require_authenticated_user: []}}
            }, %{bitmap: 2}}, :bdd_top, :bdd_bot}
      },
      plug_opts: %{atom: {:union, %{new: []}}}
    },
    closed: %{
      log: %{atom: {:union, %{debug: []}}},
      plug: %{atom: {:union, %{FooWeb.GammaController => []}}},
      conn: %{atom: {:union, %{nil: []}}},
      path_params: %{map: {{:closed, %{}}, :bdd_top, :bdd_bot}},
      route: %{bitmap: 1},
      pipe_through: %{
        list:
          {{%{
              atom: {:union, %{api: [], require_authenticated_user: []}}
            }, %{bitmap: 2}}, :bdd_top, :bdd_bot}
      },
      plug_opts: %{atom: {:union, %{index: []}}}
    },
    closed: %{
      log: %{atom: {:union, %{debug: []}}},
      plug: %{atom: {:union, %{FooWeb.GammaController => []}}},
      conn: %{atom: {:union, %{nil: []}}},
      path_params: %{map: {{:closed, %{}}, :bdd_top, :bdd_bot}},
      route: %{bitmap: 1},
      pipe_through: %{
        list:
          {{%{
              atom: {:union, %{api: [], require_authenticated_user: []}}
            }, %{bitmap: 2}}, :bdd_top, :bdd_bot}
      },
      plug_opts: %{atom: {:union, %{create: []}}}
    },
    closed: %{
      log: %{atom: {:union, %{debug: []}}},
      plug: %{atom: {:union, %{FooWeb.AlphaModelController => []}}},
      conn: %{atom: {:union, %{nil: []}}},
      path_params: %{map: {{:closed, %{}}, :bdd_top, :bdd_bot}},
      route: %{bitmap: 1},
      pipe_through: %{
        list:
          {{%{
              atom: {:union, %{api: [], require_authenticated_user: []}}
            }, %{bitmap: 2}}, :bdd_top, :bdd_bot}
      },
      plug_opts: %{atom: {:union, %{index: []}}}
    },
    closed: %{
      log: %{atom: {:union, %{debug: []}}},
      plug: %{atom: {:union, %{FooWeb.AlphaController => []}}},
      conn: %{atom: {:union, %{nil: []}}},
      path_params: %{map: {{:closed, %{}}, :bdd_top, :bdd_bot}},
      route: %{bitmap: 1},
      pipe_through: %{
        list:
          {{%{
              atom: {:union, %{api: [], require_authenticated_user: []}}
            }, %{bitmap: 2}}, :bdd_top, :bdd_bot}
      },
      plug_opts: %{atom: {:union, %{new: []}}}
    },
    closed: %{
      log: %{atom: {:union, %{debug: []}}},
      plug: %{atom: {:union, %{FooWeb.AlphaController => []}}},
      conn: %{atom: {:union, %{nil: []}}},
      path_params: %{map: {{:closed, %{}}, :bdd_top, :bdd_bot}},
      route: %{bitmap: 1},
      pipe_through: %{
        list:
          {{%{
              atom: {:union, %{api: [], require_authenticated_user: []}}
            }, %{bitmap: 2}}, :bdd_top, :bdd_bot}
      },
      plug_opts: %{atom: {:union, %{index: []}}}
    },
    closed: %{
      log: %{atom: {:union, %{debug: []}}},
      plug: %{atom: {:union, %{FooWeb.AlphaController => []}}},
      conn: %{atom: {:union, %{nil: []}}},
      path_params: %{map: {{:closed, %{}}, :bdd_top, :bdd_bot}},
      route: %{bitmap: 1},
      pipe_through: %{
        list:
          {{%{
              atom: {:union, %{api: [], require_authenticated_user: []}}
            }, %{bitmap: 2}}, :bdd_top, :bdd_bot}
      },
      plug_opts: %{atom: {:union, %{create: []}}}
    },
    closed: %{
      log: %{atom: {:union, %{debug: []}}},
      plug: %{atom: {:union, %{FooWeb.BetaController => []}}},
      conn: %{atom: {:union, %{nil: []}}},
      path_params: %{map: {{:closed, %{}}, :bdd_top, :bdd_bot}},
      route: %{bitmap: 1},
      pipe_through: %{
        list:
          {{%{
              atom: {:union, %{api: [], require_authenticated_user: []}}
            }, %{bitmap: 2}}, :bdd_top, :bdd_bot}
      },
      plug_opts: %{atom: {:union, %{list_checked_out: []}}}
    },
    closed: %{
      log: %{atom: {:union, %{debug: []}}},
      plug: %{atom: {:union, %{FooWeb.UserSessionController => []}}},
      conn: %{atom: {:union, %{nil: []}}},
      path_params: %{map: {{:closed, %{}}, :bdd_top, :bdd_bot}},
      route: %{bitmap: 1},
      pipe_through: %{
        list: {{%{atom: {:union, %{browser: []}}}, %{bitmap: 2}}, :bdd_top, :bdd_bot}
      },
      plug_opts: %{atom: {:union, %{new: []}}}
    },
    closed: %{
      log: %{atom: {:union, %{debug: []}}},
      plug: %{atom: {:union, %{FooWeb.UserSessionController => []}}},
      conn: %{atom: {:union, %{nil: []}}},
      path_params: %{map: {{:closed, %{}}, :bdd_top, :bdd_bot}},
      route: %{bitmap: 1},
      pipe_through: %{
        list: {{%{atom: {:union, %{browser: []}}}, %{bitmap: 2}}, :bdd_top, :bdd_bot}
      },
      plug_opts: %{atom: {:union, %{delete: []}}}
    },
    closed: %{
      log: %{atom: {:union, %{debug: []}}},
      plug: %{atom: {:union, %{FooWeb.UserSessionController => []}}},
      conn: %{atom: {:union, %{nil: []}}},
      path_params: %{map: {{:closed, %{}}, :bdd_top, :bdd_bot}},
      route: %{bitmap: 1},
      pipe_through: %{
        list: {{%{atom: {:union, %{browser: []}}}, %{bitmap: 2}}, :bdd_top, :bdd_bot}
      },
      plug_opts: %{atom: {:union, %{create: []}}}
    }
  )

gldubc and others added 2 commits September 28, 2025 18:49
The key change is that map_line_empty was checking the whole tree no matter what, due to an or condition that should not be checked as the previous Enum.all? are enough to conclude.

Also clarified the invariant that allows not to check the positives fields as a base case (still in map_line_empty?)
@josevalim josevalim merged commit 5a2a5ae into elixir-lang:main Sep 28, 2025
13 checks passed
@josevalim
Copy link
Member

💚 💙 💜 💛 ❤️

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.

4 participants