E-graphs are a neat intermediate representation for program transformations and optimisations, which group program terms into equivalence classes (e-classes). An optimising compiler can then select the "best" representative from each e-class during code generation.
However, in some circumstances there may be two or more e-classes, where the correctness of the generated code depends on corresponding representatives being chosen from each class; i.e. the choice of representatives is not independent. For example, suppose our program has a set of small natural numbers, and we want the compiler to consider two possible representations: either a standard set data structure, or a bitmask. In that case, we have the following semantic equivalences:
my_set = set()
is equivalent tomy_mask = 0
my_set.add(x)
is equivalent tomy_mask |= 1 << x
y in my_set
is equivalent to(my_mask & (1 << y)) != 0
In order for the generated code to be correct, the compiler must consistently choose either the former or the latter representative from each e-class. If we select my_mask |= 1 << x
from one e-class and y in my_set
from another, then the generated code will be incorrect (even if the compiler knew to declare both variables). Put another way, the members of each e-class are only equivalent depending on which representatives are chosen from other e-classes.
How can an optimisation like this be implemented correctly using e-graphs?
x
andy
must also imply an equivalence betweenf(x)
andf(y)
. Performing these rewrites (set()
=>0
,set.add(x)
=>set | (1 << x)
, etc) each individually violates this invariant and so is incompatible with e-graphs in this form. $\endgroup$egg
supports this...) However, the only way to know which commitment is more efficient would be to perform two extractions and compare their cost, which would lead to combinatorial explosion. I can write a complete answer later, if nobody else gets to it. $\endgroup$