Skip to content

Prune disjoint positives when converting map and tuple BDDs to DNF - #16046

Merged
josevalim merged 1 commit into
mainfrom
prune-bdd-dnf
Nov 3, 2026
Merged

josevalim merged 1 commit into
mainfrom
prune-bdd-dnf

Conversation

@josevalim

Copy link
Copy Markdown
Member

Converting a map or tuple BDD to DNF expanded every path and only then intersected the positive literals of each line, discarding the empty ones. When a large type is refined by unions, such as a struct with a negated guard intersected with %{m: not :pp} or %{t: not :nn}, most paths have disjoint positives and the expansion dominates. In the "from multiple defguard" test, 116 map key fetches expanded 100,834 lines, of which only 3,764 were non-empty.

We now intersect the positive literals as we traverse the BDD:

  • map_bdd_to_dnf/5 discards whole subtrees as soon as their positive literals are disjoint, which is used by map_bdd_to_dnf_remove_empty/1 and map_bdd_to_dnf_with_empty/1

  • map_bdd_empty?/4 does the same for map_empty?/2, stopping as soon as it finds a non-empty line, in the same order as before

  • tuple_bdd_to_dnf/4 and tuple_bdd_empty?/4 do the same for tuples, pruning on disjoint sizes, while the emptiness of the elements is still checked once per line

The positive literals are now intersected from the root down, instead of from the deepest literal up, which yields the same types.

This brings the "from multiple defguard" module from 660ms to 390ms and is neutral on ts_bench projects.

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

Converting a map or tuple BDD to DNF expanded every path and only then
intersected the positive literals of each line, discarding the empty
ones. When a large type is refined by unions, such as a struct with a
negated guard intersected with `%{m: not :pp} or %{t: not :nn}`, most
paths have disjoint positives and the expansion dominates. In the
"from multiple defguard" test, 116 map key fetches expanded 100,834
lines, of which only 3,764 were non-empty.

We now intersect the positive literals as we traverse the BDD:

  * `map_bdd_to_dnf/5` discards whole subtrees as soon as their
    positive literals are disjoint, which is used by
    `map_bdd_to_dnf_remove_empty/1` and `map_bdd_to_dnf_with_empty/1`

  * `map_bdd_empty?/4` does the same for `map_empty?/2`, stopping as
    soon as it finds a non-empty line, in the same order as before

  * `tuple_bdd_to_dnf/4` and `tuple_bdd_empty?/4` do the same for
    tuples, pruning on disjoint sizes, while the emptiness of the
    elements is still checked once per line

The positive literals are now intersected from the root down, instead
of from the deepest literal up, which yields the same types.

This brings the "from multiple defguard" module from 660ms to 390ms
and is neutral on ts_bench projects.

Assisted-by: Claude Code:claude-opus-5-5
@josevalim
josevalim requested a review from gldubc November 2, 2026 18:14
@josevalim
josevalim merged commit 953e69f into main Nov 3, 2026
26 checks passed
@josevalim
josevalim deleted the prune-bdd-dnf branch November 3, 2026 13:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants