{"slug": "lifting-e-graphs", "title": "Lifting E-Graphs", "summary": "A researcher's talk on 'Lifting E-Graphs' was accepted at the EGRAPHS workshop, introducing a new approach to handling variable contexts in e-graphs by treating context as part of the term itself. The method addresses issues with explicit variable names, including generative process divergence, missed sharing, and unintended conflation of distinct mathematical objects.", "body_md": "# Lifting E-Graphs\n\nI submitted a talk to the EGRAPHS workshop and it was accepted! [https://pldi26.sigplan.org/details/egraphs-2026-papers/13/Lifting-E-Graphs-A-Function-Isn-t-a-Constant](https://pldi26.sigplan.org/details/egraphs-2026-papers/13/Lifting-E-Graphs-A-Function-Isn-t-a-Constant)\n\nBetween the time when I submitted my abstract and now, while the meat of “there is something here” has not changed, my understanding and best explanation of the thing has been refined.\n\nThe whole design is powered by intuition coming from a semantic model manipulating `R^n -> R`\n\nfunctions in a systematic way. I’m now calling what I previously called a Thinning e-graph [https://www.philipzucker.com/thin_egraph/](https://www.philipzucker.com/thin_egraph/) a Lifting E-Graph, as the word `lift`\n\nis more evocative of what the main operation of interest does on `R^n -> R`\n\nfunctions.\n\n# What’s Wrong with Explicit Names?\n\nThere are 3 issues with explicit names\n\n- Generative processes can run off the rails (eqsat). We need fresh names sometimes. As an example\n`P --> forall x, P | where x fresh`\n\nis basically a valid rewrite for propositions. Sometimes rewrites like this can be useful. We might derive the same thing many many times redundantly with different names if we just gensym them. We can play skolem games and free variable analysis games to try and derive names we know are fresh and repeatable, but this is (subjectively) inelegant. - Missed sharing.\n`f(g(h(x)))`\n\nshares no storage with`f(g(h(y)))`\n\n. The amount of missed storage opportunity gets worse the deeper and bigger the term. These two things*aren’t equal*, so I’m not exactly complaining about missed*equality*. I’m complaining about a missed*relationship*that can be used for reasoning and compaction. - Too much sharing. This is the surprising one. I think non careful use of explicit names conflates actually disequal objects. The notation\n`sin(x)`\n\nactually can refer to distinct mathematical objects that one should at least be aware of the possibility to disambiguate.\n\nThe more surprising issue I think is #3. Let’s expand upon that.\n\n# Original Sin: X\n\n`sin(x)`\n\nfrom an ordinary perspective is fine. We’ve lived with the notation for hundreds of years. It works. We know what it means when we see it.\n\nFrom another perspective it is horribly vague and possibly collapsing distinct entities. We are somehow referring vaguely to the function `sin`\n\n, but are we specifically referring to `x |-> sin(x) : R -> R`\n\nor to `x,y |-> sin(x) : R^2 -> R`\n\n?\n\nAccording to a simply typed perspective of the world, these are different things, and in fact can’t even be compared for equality due to type mismatch. The graphs are completely different images. Yet, the notation that suppresses the context `x,y |->`\n\nor leaves it implicit conflates these two things. It is at least worrying that perhaps in some subtle way this conflation is in essence asserting `R^2 = R^1`\n\nand from thence chaos ensues.\n\nFrom this observation come a slogan for today’s design philosophy:\n\n**Context is not where a term is, it is part of what a term is**\n\nThis is not the case for every conception of the word “context”, but it is what I want to do today.\n\nWe will choose to *not* leave out the information of `x,y |->`\n\n*ever*. It is a part of the thing we are discussing. For today, we shall consider it basically incoherent to talk about a “bare” `sin(x)`\n\n# Naive Nameless\n\nThere is a naive way to achieve this design philosophy using an ordinary egraph / ordinary first order terms.\n\nWe can make a different copy of every function symbol for every dimension/context we might be working in and we can refer to variables by (dimension,index) pairs $x_{di}$ rather than by names. For example $x_{10}$ (the zeroth variables in context of size 1) is what previously I would have called `x |-> x`\n\n, $x_{21}$ (the first variable in context of size 2) is what previously I would have called `x,y |-> y`\n\n.\n\nLikewise, we could also disambiguate all the `sin`\n\ninto different versions $\\sin_d$ depending on the type of it’s argument. If $x_{21}$ has type $R^2 \\rightarrow R$ then if `sin`\n\nis going to accept it, it needs to take in arguments of that type. We have $sin_0 : (R^0 \\rightarrow R) \\rightarrow (R^0 \\rightarrow R)$, $sin_1 : (R \\rightarrow R) \\rightarrow (R \\rightarrow R)$, $sin_2 : (R^2 \\rightarrow R) \\rightarrow (R^2 \\rightarrow R)$ and so on.\n\nReally all of these come from the pointwise application of the regular `sin`\n\nfunction, and this is a parametric polymorphic construction, so this disambiguation is not really that necessary (the index `n`\n\nis derivable from the dimension of `sin`\n\n’s arguments). Still, if we wanted to stay conceptually in a simply typed framework, this is what we’ve go to do.\n\nOk, great. This carefulness does solve issue #3 of too much sharing. At the same time, we’ve made point #2 of too little sharing both better and worse.\n\nBecause we are being nameless by referring to variables by integers, `x |-> f(g(h(x)))`\n\nbecomes syntactically the same as `y |-> f(g(h(y)))`\n\nsince both become `f1(g1(h1(x10)))`\n\n. So sharing has become better in that sense.\n\nOn the other hand, now `f1(g1(h1(x10))) : R -> R`\n\nand `f2(g2(h2(x20))) : R^2 -> R`\n\nshare absolutely no storage, despite being very similar (again, not *equal*, because they don’t even have the same type). Perhaps before we could have referred to both as `f(g(h(x)))`\n\n, so out ability to share storage for these two cases has gotten worse.\n\nWhat to do about that?\n\n# Lifting\n\nWell, let’s discuss the semantic sense in which `f1(g1(h1(x10))) : R -> R`\n\nand `f2(g2(h2(x20))) : R^2 -> R`\n\nare related.\n\nThe latter is a lifted version of the former.\n\nIf you gave me an object `f1(g1(h1(x10))) : R -> R`\n\nI could produce the object `f2(g2(h2(x20))) : R^2 -> R`\n\nby merely throwing away the second argument and propagating the first argument. As a python function, this lifting combinator would be written as `lambda f: lambda x,y: f(x)`\n\n. This is a lifting operation, and lifting operations have a tractable algebra associated with them. [https://www.philipzucker.com/thin1/](https://www.philipzucker.com/thin1/)\n\nInstead of ever storing `f2(g2(h2(x20))) : R^2 -> R`\n\n, I could instead store `lift_10(f1(g1(h1(x10)))) : R^2 -> R`\n\n. The subscript on `lift`\n\nis a bitvector with a 1 if I should keep the argument, or a 0 if throw it away. Again, this all perfectly first order syntactic and simply typed. I could do so as an encoding in a regular egraph. Sharing of substructure is achieved because the two semantically distinct things now share big subterms.\n\nLifting has some useful properties that one would then encode as rules. The “parametric polymophism” of typical pointwise derived combinators like `sin`\n\nmanifests as a rewrite rule `sin(lift_i(X)) = lift_i(sin(X))`\n\n. This is stating that `lift`\n\nis a homomorphism with respect to typical function symbols / it kind of sort of commutes through them.\n\nIn addition, there is sort of a constant propagation rule for liftings `lift_i(lift_j(X)) = lift_k(X) where k = i . j`\n\nIn short, the system we are discussing can be encoded as explicit first order `lift`\n\ncombinators with\n\n`lift_i(lift_j(X)) = lift_k(X)`\n\nlift compaction rules`f(lift_i(X), lift_i(Y)) = lift_i(f(X,Y))`\n\nlift pulling rules\n\n# Baking Lift In\n\nThese properties of lifting are so simple, ubiquitous and structural that it may make sense to bake them into the very fabric of what a term or an egraph *is*. This is sweetened by the fact that liftings/thinnings can be represented as compact bitvectors.\n\nLift is quite simple to represent as a bitvector (a thinning) with a 1 for variables to keep and 0 for variables to drop.\n\nThis is pretty compact data, and it isn’t totally crazy I think to steal some bits to pack it alongside the typical eid term/eclass identifier.\n\n## Smart Constructor For Lifting\n\nThe homomorphism rules can be oriented to float the liftings as high a possible `f(lift_i(X), lift_i(Y)) -> lift_i(f(X,Y))`\n\n. This is a natural rewrite ordering in that the right hand side is the smaller term. A Knuth Bendix Order will achieve this. You should also set the precedence of `lift`\n\nto be low to fix the marginal unary `f(lift(X)) -> lift(f(X))`\n\ncase.\n\nThe smart constructor operation will ensure that whenever you build a node with arguments eids that are lifted more than necessary, you get back a fat eid handle to the same interned data regardless of the extra lifting, enabling reduced memory usage and faster lifting relationship comparison.\n\nWhenever you build a new enode, the smart constructor should examine the common lifting of the fat eids of it’s arguments, peel off this lifting, intern the enode, and the put the common lifting back on before returning the fat eid to the user. This is a mechanical way of achieve the lift pull up rule inside the egraph.\n\nAs described, this mechanism and the rewrite rule behind it seems pretty elementary and non mysterious to me. It has taken some time and explanation shifting to feel that way.\n\nIn the absence of a union find, this lifting pulling smart constructor + fat id makes for an interesting “alpha aware” hash cons. [https://www.philipzucker.com/thin_hash_cons_codebruijn/](https://www.philipzucker.com/thin_hash_cons_codebruijn/)\n\nPulling lift up corresponds in an interesting way to the co-De Bruijn style of normalizing and representing lambda terms as described in McBride’s Everybody’s Got to Be Somewhere [https://arxiv.org/abs/1807.04085](https://arxiv.org/abs/1807.04085) . I have not discussed lambdas at all thus far. I think the considerations of this post are more elementary and that lambdas/binding forms are a layer to add to this more elementary layer.\n\nNote also that by being as thin as possible, the dimensionality can play kind of a nameless free variable analysis. By being part of the fabric of what the term even *is*, it is less of a problem to make sure that the free variable analysis is up to date before you make some dicey variable rewrite.\n\n## Lifting Union Find\n\nBut we want an egraph. We need to add a union find to that hash cons.\n\nHow do we implement a union find that accepts lifted fat eids?\n\nBecause liftings are semantically injective functions, when you union two lifted eids `lift_i(a) = lift_i(b)`\n\n, you can peel off the common parts of their liftings and learn `a = b`\n\n.\n\nThis is similar to the move you can make in syntactic unification or from equality between algebraic datatypes, which are also injective functions. `cons(a, c) = cons(b, c)`\n\nimplies `a = b`\n\n.\n\nIf what is left is bare eids `e6 = e47`\n\nbecause the two liftings were identical, then it is the ordinary union find action at that point.\n\nIt does not even type check to union two objects with different numbers of variables, if a lifting mismatch is due to this, it is a user error.\n\nHowever, there still remains legitimate cases of unequal thinnings being unioned.\n\n`x * 0 = 0`\n\nand Redundancies\n\nA concerning counterexample to any discussion of variables in e-graphs is `x * 0 = 0`\n\n. If this is a truly bidirectional equality, it allows `x`\n\nto slip into any location where `0`\n\nis used. This feels like a non hygienic scope extrusion, since `0`\n\ncan appear anywhere, including places where `x`\n\nmay not be in scope. Consider using a naive first order encoding of lambdas\n\n```\n  lam(y, y * 0)\n= lam(y, 0)         by y * 0 = 0\n= lam(y, x * 0)     by 0 = x * 0\n```\n\nFeels bad.\n\nOne can perhaps argue for a semantics for which accessing an `x`\n\nnot in scope is not a violent error, but instead just returns arbitrary (well typed?) junk. In this case, it is indeed semantically fine to access `x`\n\nif you’re going to just destroy all that information immediately by multiplying by 0.\n\nStill, this semantics fills me with (subjective) discomfort. It feels inelegant. Maybe I’m just a coward.\n\nNo, scratch that. I’m *definitely* a coward. But it in unclear if this discomfort is a manifestation of my cowardice or not.\n\nWhile I have toyed with the idea of allowing junking / dumping moves [https://www.philipzucker.com/dump_calculus/](https://www.philipzucker.com/dump_calculus/) that are pseudoinverses to lifting, my current understanding is that it is not necessary to do so, and I think a bit more elegant to avoid it.\n\nFrom the careful scoping/lifting perspective, the actual equation in question is `x |-> x * 0 = 0`\n\nwhich is combinatorized into `x_10 * lift_0(0) = lift_0(0)`\n\n. Note that since `0`\n\nis a constant (a 0-arity function `[] |-> 0`\n\n), it must be lifted into current 1-context `x |->`\n\n.\n\nBoth `x_10 * lift_0(0)`\n\nand `0`\n\nare actually interned as eids, let’s say `x_10 * lift_0(0) -> e47`\n\nand `0 -> e6`\n\n, so the union occurring is `union(e47. lift_0(e6))`\n\n. We do indeed have differing lifts on the left and right side.\n\nThe union find can resolve this by picking the orientation / parent to be `e6`\n\n. This results in the rule `e47 -> lift_0(e6)`\n\nwhich is representable in a lifting annotated `parents`\n\ntable, akin to how `e48 -> e14 + 7`\n\ncan be stored in an integer offset annotated parents table in a group union find.\nPicking this directionality is required because it “solves” for `e47`\n\n. There is no lifting that will solve for `e6`\n\nin terms of `e47`\n\n, `e6 -> lift_?(e47) DOESN'T WORK`\n\n. Being in solved form is what enables the simple action of `find`\n\naccumulating an annotation as it traverses the parents table.\n\nIt is also possible to be in a situation which neither left or right side is solvable in terns of the other. This situation is luckily still solvable by generating a common fresh constant that both left and right are solvable to `left -> annot1(fresh)`\n\nand `right -> annot2(fresh)`\n\n.\n\nThe same kind of consideration come in a more elementary was from baking in integer constant multiplications into a union find like `3 * e47 = 2 * e3`\n\n. More discussion on this lightly asymmetric annotated union find here [https://www.philipzucker.com/thin_monus_uf/](https://www.philipzucker.com/thin_monus_uf/)\n\nAs an example that requires this fresh constant generation, consider `x,y |-> x*0 = 0*y`\n\nwhich combinatorizes to `lift_10(x10 * lift_0(0)) = lift_01(lift_0(0) * x10)`\n\n. This ought to be a rarer occurrence and I am almost inclined to not implement it. Neither can be solved in terms of the other, but by generating a fresh eid, there is semantically something that both ought to be solvable to. Let’s say the right hand side has eid `e14`\n\n, `lift_0(0) * x10 -> e14`\n\n. Then we infer there exists a fresh `e112`\n\nsuch that `e14 -> lift_0(e112)`\n\nand `e47 -> lift_0(e112)`\n\n. Indeed, `e112`\n\nis semantically the same as `0`\n\nand if we assert the `x |-> x * 0 = 0`\n\nand `x |-> 0 * x = 0`\n\nfirst (which is probably the more natural thing to do), the initial example equation `x,y |-> x*0 = 0*y`\n\nwould be considered redundant.\n\nThis example is somewhat constructed just to show how irreconcilable liftings can occur in a semantically valid way. I am not convinced it is natural to write rules in such a way that a situation like this would show up very often\n\n# E-Matching\n\nAll of the implementation of a lifting egraph was pretty mechanical without thinking too deeply until I hit e-matching. That is what sent me back to the drawing board to clarify the semantics and first order term model of what I was doing.\n\nA confusing thing is that we have made liftings baked in and somewhat implicit. Should liftings appear in patterns? When should liftings be allowed to be inserted to solve the problem?\n\nI think what I now understand is that there are at least 3 different e-matching problems you might want to solve.\n\n`0 = ?x * ?y`\n\nonly matching on unlifted canonical id`lift_010(0) = ?x * ?y`\n\nmatching a lifted id`?lift1 0 = ?lift2 (?x * ?y)`\n\nmatching an TBD arbitrarily lifted id.\n\nThe last one is lightly a unification problem (has variables on both sides of the equations), so maybe we can consider that out of scope. It is a somewhat natural thing to ask though.\n\nBy far the simplest thing to do, and I think it kind of makes pragmatic sense, is that as soon as you encounter a thinning from the root in the union find during ematching, just don’t traverse that edge. This kind of corresponds to only solving problem 1. The only nodes you’re going to find down that edge will contain redundant variables. It is rarely a good thing to have extra unnecessary variables around (they are expensive for all kinds of reasons), so why even do it? In this case, e-matching is basically identical to ordinary ematching but you do have to compose the thinnings as you are traversing down the argument edges of an enode.\n\nOk, but let us say we want to do it.\n\nThe pattern `lift_00(0) = ?x * ?y`\n\nwith `x10 * lift_0(0) = lift_0(0)`\n\nin the egraph should have 2 solutions, `{?x -> x20, ?y -> 0}`\n\nand `{?x -> x21, ?y -> 0}`\n\n. This corresponds to matching against the target `x,y |-> 0`\n\nand getting the two solutions `x,y |-> x * 0`\n\nand `x,y |-> y * 0`\n\n. If we pattern match over an higher lifting of 0 like `lift_0000(0)`\n\n, we’d expect 4 solutions, and so on. These are enumerating all the variables you can place in that spot that get annihilated by 0. In terms of the liftings, they are all the ways of solving the equation `lift_00 = lift_? . lift_0`\n\nwhich has two solutions `lift_10`\n\nand `lift_01`\n\n. In words, to throw away both arguments `lift_00`\n\n, you can either throw away the second argument and the one left `lift_10 . lift_0`\n\n, or you can throw away the first argument and then throw away the one left `lift_01 . lift_0`\n\n.\n\n`lift_0 = lift_? . lift_00`\n\nproblems of this shape have no solutions in liftings. Liftings monotonically increase the number of arguments, and `lift_00`\n\nhas already lifted to taking in 2 redundant arguments whereas the left hand side `lift_0`\n\nonly takes in 1 redundant argument.\n\nFully writing out the derivations of these two matches:\n\n```\nlift_00(0) =? ?x * ?y\nlift_01(lift_0(0)) =? ?x * ?y               by lift factoring (motivated by the next move)\nlift_01(x10 * lift0(0)) =? ?x * ?y          by  lift_0(0) = x10 * lift0(0)\nlift_01(x10) * lift_01(lift0(0)) =? ?x * ?y by  mul-homomorphism   lift(mul(X,Y)) = mul(lift(X), lift(Y))\n?x -> lift_01(x10), ?y -> lift_00(0)        by the usual syntactic matching of *\n```\n\nBut you can also use the other factoring\n\n```\nlift_00(0) =? ?x * ?y\nlift_10(lift_0(0)) =? ?x * ?y               by the other lift factoring (motivated by the next move)\nlift_10(x10 * lift0(0)) =? ?x * ?y          by  lift_0(0) = x10 * lift0(0)\nlift_10(x10) * lift_10(lift0(0)) =? ?x * ?y by  mul-homomorphism   lift(mul(X,Y)) = mul(lift(X), lift(Y))\n?x -> lift_10(x10), ?y -> lift_00(0)        by the usual syntactic matching of *\n```\n\n# Bits and Bobbles\n\n## The Picture\n\nSome of the e-graph’s success it due to this picture\n\nLike in my picture here [https://www.philipzucker.com/egraph2024_talk_done/](https://www.philipzucker.com/egraph2024_talk_done/)\n\nwe do not want to represent eclasses as a dotted boundary, but instead as dotted arrows representing the union find parents relation. Then the thinnings that appear inside the union find can be represented visually.\n\nThe thinning egraph can be visualized by thickening all the edges (both solid child edges and dashed union find edges) into “buses” like a digital circuit. These buses have N lines in them for the N variables in context. The 0 in a thinning appears as lines that end in an cross out `x`\n\nin between nodes. Inside the bus, lines never cross each other because thinnings are about strictly monotonic ordered mappings.\n\nHere is an example lifting egraph resulting from `x,y |-> x * y = y * x`\n\naka `lift_10(var) * lift_01(var) = lift_01(var) * lift_10(var)`\n\n. The thinning on the union find edge is an identity thinning, so it has no lines endings inside it.\n\nToday I also saw [https://www.csl.sri.com/papers/bachmairtiwari00/cade00-CC.pdf](https://www.csl.sri.com/papers/bachmairtiwari00/cade00-CC.pdf) abstract congruence closure by Bachmair and Tiwari which has a similar kind of diagram\n\n## Knuth Bendix Model\n\n```\ncnf(mulname, axiom, mul(id, l(zero)) = e0).\ncnf(union, axiom, e0 = l(zero)).\n\ncnf(mul_zero_id, axiom, mul(zero,zero) = e2).\ncnf(mulzerozero, axiom, e2 = zero).\n\ncnf(linj, axiom  , l(X) != l(Y) | X = Y).\ncnf(lift_mul, axiom, l(mul(X,Y)) = mul(l(X),l(Y))). % homomorphism\n```\n\nA simplified model with only a single lifting `l`\n\n(which I think can be interpreted as roughly `lift_1111110`\n\n, a family of thinnings that always drops that last (or first) variable) was very helpful for me to play around on paper.\n\nEprover is flexible enough to add an injectivity axiom and state the homomorphism lifting pullthrough axiom. Combined with ground equations, this seems to complete with a knuth bendix ordering with `weight(e) = semantic dimension`\n\nand `w(l) = 1`\n\nand `l`\n\nlow in the precedence. The weight proportional to dimension and precedence is necessary to have equations orient into `e47 -> l(e6)`\n\nin the appropriate way with all the thinnings on the right.\n\nI am not entirely sure that this recipe for the ordering will always complete, but it feels plausible (70%) to me.\n\n## Comparing with Slotted\n\nI’ve been discussing with Rudi a lot about slotted e-graphs and we’ve been trying to find the middle ground of this thinning perspective and the slotted perspective.\n\nThe difference is very evocative of two major camps dealing with bindings: nominal [https://www.cl.cam.ac.uk/~amp12/papers/nomlfo/nomlfo-draft.pdf](https://www.cl.cam.ac.uk/~amp12/papers/nomlfo/nomlfo-draft.pdf) vs presheafy/functorial/family [https://dimasamoz.github.io/docs/talks/2024-04-WG6.pdf](https://dimasamoz.github.io/docs/talks/2024-04-WG6.pdf) (for lack of better words) stuff. I think Rudi has his own sources, but my awareness of thinnings does come filtered down through interconnections with this previous work (via McBride’s mastodon comments). Direct exposure to those papers does me little good though, since I have no idea what they are talking about. I am not a category theory guy.\n\nWe’ve been tinkering [https://github.com/philzook58/slotteduf](https://github.com/philzook58/slotteduf) on “ordered slotted” which seems to be an intermediate construction that lives somewhere in the world between slotted and liftings egraphs.\n\nThinnings have at least two representations. the bitvector representation `10001`\n\nor just listing the indices of the keeps `[0,4]`\n\nare *close* to being isomorphic. Attaching this latter representation to eids is seemingly roughly the same as a slotted eid.\n\nThe reason they are not isomorphic is that we have lost the total length of the bitvector. We do not know if `[0,4]`\n\norresponds to `10001`\n\nor `100010`\n\nor `10001000000000000...`\n\n. I am inclined to interpret the slotted notion of thinning `[0,4]`\n\nas mediating from a common countably infinite domain of variables (the thinning with infinite trailing zeros). Everything is in ultimately considered in the same mega context and you have global names (number labels) for stuff. Something something (co)slice category. Rudi seems inclined to consider it to represent the shortest length bitvector `10001`\n\nRudi’s interpretation of `[0,4]`\n\nis that the eid has been *applied* to slot arguments `[0,4]`\n\nwhich is hard to see in my model.\n\nBefore interning, slotted egraphs perform a canonical shape computation argument by argument to pull out permutations. An ordering is figured out for the slots by using the structure of the node.\n\nIf slots are considered to be ordered and permutations no longer baked in, shape computation becomes much easier (at the same time the egraph is not doing permutation stuff for you, so you are perhaps losing something. This is a tradeoff, not a free lunch). You instead take all the slots `[4,7,100]`\n\nand just keep them in order but now label them 1 through n `[0,1,2]`\n\nwith a thinning that says 0 maps to 4, 1 maps to 7 and 2 maps to 100.\n\nMcBride did have some mysterious comment about a ordered universe of constants as being nice.\n\nThe names vs nameless isn’t perhaps exactly the axis on which the difference lies. One can also have ordered slots, and one can have named thinnings as evidence for the subrecord relation rather than the subsequence relation.\n\n## Binders\n\nLambda or binder is another smart constructor, but one that needs to peel off the variables bound from the eid of the child while it is pulling the lifting annotation up. Lambda does not pure commute with liftings, it changes the lifting as it comes up through.\n\nIn a curious way, lambdas are like a projection. They *remove* a variable, the same way filling it in with 0 might do. They do some by also changing the type of the object to be a functional type, so this “projection” is not lossy.\n\n## Connections\n\nLiftings are indeed exactly thinnings and weakenings and `|->`\n\nis the same as `|-`\n\n. I just think the change of terminology and notation helps make the description easier to digest. To my ear, thinning and weakening seem to connote the exact opposite of the operation I think is happening, taking a low dimensional function and turning it into a high dimensional one.\n\nDeeper waters:\n\n- Semi simplicial categories\n- Cubical categories\n- Families\n- Presheaf models\n- Explicit Weakening calculi.\n[https://arxiv.org/abs/2412.03124](https://arxiv.org/abs/2412.03124)[https://hal.science/hal-00384683/document](https://hal.science/hal-00384683/document) - partially applied composition\n`sin .`\n\nand Yoneda stuff\n\nThere are connections here. What I have done is tried to distill away as much complexity as possible. I think lifting egraphs stand alone without these things, but the connections may enrich your understanding. Everyone has different stuff that works for them. I am not a math or theory maximalist. I have aspects I like, aspects I understand, aspects I don’t like, aspects I don’t understand, aspects I wish I understood.\n\nI don’t think I actually am good at abstraction. What I can do is have 3 concrete examples in superposition in my head, which is a little different than having a black box conception. This superposition may explain why I punctuate and use notation so inconsistently. Rapid oscillation between python, pseudo-haskell, term rewriting, pictures, and latex is how I am.\n\nNominal vs Nameless\n\nThe assymetric union find as a way of storing proof relevant tree (forest) like partial orders. The tree thing makes sense because union finds are trees. The proof relevant part is necessary because the proof data (edge annotation) tells you where to plug in a new fact into your trees. Positive offset and integer divisibility do have this proof relevant partial order flavor.\n\nIn this sense, perhaps the asymmetric union find is a nice version of a inequality union find that does not require search. [https://www.philipzucker.com/asymmetric_complete/](https://www.philipzucker.com/asymmetric_complete/)\n\n## Bits and Bobbles\n\nIt isn’t persay terrible to assume that `sin(x)`\n\nuniquely corresponds to always talking about $R^\\infty \\rightarrow R$ or similarly `(Name -> R) -> R`\n\n, an environment passing semantics. It’s a different style. It does seem off to bring names into the discussion or bring infinity into the discussion in order to discuss `R^2 -> R`\n\n. It is reminiscent of the age old synthetic vs analysis approach. Intrinsic vs extrinsic. Is a sphere best described by cutting it out of R^3 or by piecing together 2d coordinate patches? What is “best”? What is most beautiful? What fits the clunky notation or framework you’ve got? The lifting approach is a bit more “well typed”. “Well typed” often kind of sucks though. It is brittle and you can end up in type level programming hell if you don’t use taste. You’ve pushed the pain from the ordinary stuff to the type level stuff, which is automatic. But when it stops being automatic, now you’re just working in a worse environment.\n\nEncoding is inelegant and each tiny inelegance chokes you the further you try and go with it. Ultimately, inelegance is a limitation on scale. This limitation is soft and therefore even more insidious. I can’t argue against people who don’t feel the fear of soft limitations. There will never be an inarguable point at which you say “I can go no further”, but the bog always wins. You lie down, the mud and reeds enters your mouth and you die. Encodings are the complexity demon opening it’s jaws and grinning.\n\nThe too little and too much sharing story is reminiscent of the Hash Cons Modulo alpha paper.\n\neids are often 32 or 64 bits, and typical egraphs do not grow to that size, leaving some headspace for at least of byte of metadata in there, maybe more. You could also perhaps play some run length encoding tricks, etc, depending on the nature of the thinnings you expect. If you need more than 8 variables in scope at once, you could reify the lifting to an enode like the encoding above or start allocating bit vectors kind of like a bigint implementation.\n\nIt is not necessary to encode these properties as rules (although perhaps one still should if you want to retrofit this into a preexisting e-graph system as an encoding).\n\nThis is not exactly the same as how I developed the ideas though. The ideas developed from just the implementation mechanism of thinnings as bitvectors seeming so clean and useful.\n\n`x |-> x : R -> R`\n\nis the identity function. `x,y |-> x : R^2 -> R`\n\nis a projection function that extracts the first dimension. All bare uses of variables can be expressed as liftings of the identity function. Because of this, we only need to intern a single variable For example `x51`\n\n(the 1th variable in context of size 5) is defined to be `x51 := lift_01000(x10)`\n\nor another example `x40 := lift_1000(x10)`\n\n(remember that I have chosen the convention of 0 indexing variables). We could give `x10`\n\na special name, such as `var`\n\nor `id`\n\n.\n\n`sin(x)`\n\ncan also be given meaning according to an environmant passing semantics or more subtly by saying that `x`\n\ncan take on different values in the model but `sin`\n\nis pinned to mean mathematical sine. This is the sort of thing that happens when you have uninterpred constants like `x`\n\ncombined with inteprreted functions / constants in an SMT solve.\n\nTop down e-matching is using the `memo`\n\n`f(l(e3), l(e5)) -> l(e17)`\n\nand union find rules `e4 -> l(e6)`\n\nin the opposite of their natural direction. We expand memo to turn an eid into an enode, and we run the union find in reverse to enumerate the eids in the eclass.\n\nIf we are matching `lift_001(e17)`\n\n, first we can enumerate the eids in the eclass by running the union find backwards. If the edge in the union find has a thinning that\n\n- Discomfort about scope escape. What to do about\n`x*0 = 0`\n\n? What about when you want to push a term through a binder (change scope a little bit)?\n\nHmm. I feel like I’m flip flopping between a picture where eids correspond to specific enodes and not. It depends on whether we want the enode rules to be of the form `f(l(e)) -> e`\n\nor if we allow `f(l(e)) -> l(e)`\n\nwhere the eids can be destructively updated in the memo table. In terms of an enode list, refusing to travese redundances for ematching would correspond to just ignoring enode that has a lifted eid.\n\n`l0(e6) =? ?x * ?y`\n\nWe can follow the union find edge backwards\n`e47 -> l0(e6)`\n\nto\n\n`e47 =? ?x * ?y`\n\nThen expand the node through usjing memo table entry `x10 * l0(e6) -> e47`\n\nto\n\n`x10 * l0(e6) =? ?x * ?y`\n\nThen apply an ordinary mathing move to\n\n`x10 =? ?x, l0(e6) =? ?y`\n\nwhich can be oriented into a substitution.\n\n`e6 =? ?x * ?y`\n\ncannot use `e47 -> l0(e6)`\n\nbecause there is no `l0`\n\nto peel off.\n\n`l_00(e6) =? ?x * ?y`\n\n. `l_00`\n\ncan be decomposted to two way to expose `l_0(e6)`\n\n`l10 . l0 = l00`\n\nand `l01 . l0 = l00`\n\n- `0 = ?lift (?x * ? y)\n- `lift_0(0) = (?x * ? y)\n\nE-matching confused the hell out of me and sent me out spinning\n\n`e47 -> lift_00(enew)`\n\nwhere `lift_00`\n\nis coming from the intersection of the two lifting bitvectors `lift_10`\n\nand `lift_01`\n\n(the example is a little too cute and conflates a few things).\n\nNote change this example to `x,y |-> x*0 = 0*y`\n\nIt is also possible to union `x,y |-> x*0 = y*0`\n\nwhich combinatorizes to `lift_10(x10 * lift_0(0)) = lift_01(x10 * lift_0(0))`\n\n. This ought to be a rarer occurrnce and I am almost inclined to not implement it. Neither can be solved in terms of the other, but by generating a fresh eid, there is semantically something that both ought to be solvable to. `e47 -> lift_00(enew)`\n\nwhere `lift_00`\n\nis coming from the intersection of the two lifting bitvectors `lift_10`\n\nand `lift_01`\n\n(the example is a little too cute and conflates a few things).\n\n``` python\nfrom dataclasses import dataclass, field\n\ntype Thin = list[bool]\ntype Id = tuple[Thin, int]\n\ndef ctx(self, x : Id) -> int:\n    return len(x[0])\n\n@dataclass\nclass Node:\n    f : str\n    args : tuple[Id, ...]\n\n@dataclass\nclass EGraph:\n    memo : dict[Node, int] = field(default_factory=dict)\n    uf : list[Id] = field(default_factory=list)\n\n    def app(self, f : str, *args) -> Id:\n        assert all(ctx(arg) == ctx(args[0]) for arg in args)\n        N = ctx(args[0])\n        common = [False] * N\n        for arg in args:\n            common = [c or a for c,a in zip(common, arg[0])] # bitwise or of all arguments\n        newargs = []\n        for arg in args:\n            arg = \n        node = Node(f, tuple(args))\n        id = self.memo.get(node)\n        if id is None:\n            id = len(self.)\n            self.memo[node] = \n        else:\n            return (comp(common, id[0]), id[1])\n    def var(self, n : int, j : int):\n        thin = [False] * n\n        thin[j] = True\n        return (tuple(thin), self.memo.get(()))\n\n    def find(self, x : Id) -> Id:\n        ...\n    def union(self, x : Id, y : Id) -> Id:\n        assert ctx(x) == ctx(y)\n\n    def nodes_in_class():\n\n    def ematch(self):\n```\n\nVery confused what I am looking at with the ordered slotted egraph.\n\nSound ematching, not complete.\n\nMaybe I shouldn’t traverse redudnancy edges during ematching anyway, because those matches probably suck. They are going to be searching through dump expressions.\n\nSeed roots with non dump redundant pieces. This also means rebuilding may not want to compact away as much.\n\nequivalraince l(f(X,Y)) = f(l(X), l(Y)) is used to pull l up during canonization, but also can be used during ematching to pull l into the current target term to get f heads to match l(f(e1,e2)) =? f(X,Y) f(l(e1), l(e2)) =? f(X,Y) {X =? l(e1), Y =? l(e2)}\n\ntraversing up e -> ll(etop) becomes etop -> ddd(e) which is not fun.\n\n((X*Y)*Z) associativity is no problemo.\n\nOk what about lambda then. Lambda is not equivariant. lam(l1(B)) = l2(lam(B))\n\nl(e) = f(X,g(Y,Z)) l(f(e1,e2)) = f(X,g(Y,Z)) f(l(e1),l(e2)) = .. l(e1) = X, l(e2) = g(Y,Z)\n\nl being least in lpo does track. That would pull l to the outside. l(f(a,f(b,c))) dump is uneccessary?\n\nWhat if l was just add a variable to front. Always l1 l0 with the rest being understand to be true.\n\nl0(l1(l0()))\n\ncoherent. uhh. No. But this won’t let us do as much sharing. y = l0(l1(v)) x =? l1(l0(v))? no.\n\nshift(l)\n\nl(n, m, X)\n\nHuh. No.\n\npulling l outward but always e -> l(e) is tought to get to happen.\n\ndump nodes floordiv(x, 3) is kind of a skolem node. We could just not normalize these further. (even thought dump dump probably merges and dumps should push down)\n\nl\n\n`e -> l(e)`\n\nthen we want `e > l`\n\n`f(l(X)) -> l(f(X))`\n\nthen we want `f > l`\n\nSo `l`\n\nhas to be least in the ordered. Every eid should have a weight corresponding to it’s size.\n\nIn this simplified subsystem, I can’t have incompatable l0(e) = l1(e).\n\nx != y % not sure it can do much with this. swap(x) = y swap(y) = x swap(swap(X)) = X\n\nf(e1(x,y), ) -> e2(x)\n\nswap(e2(x)) = e2(y) ordering the slots. Is this the thing rudi and I did? maybe. x > y > z, normalize all e. Is there a point to e(x,y,z) form? We just know it’ll be 0,1,2?\n\nx + y = x + swap(x)\n\ne(x,y) = e2(y) e(x,y) = swap(y)\n\ne -> swap(e) implies e > swap swap weight = 0? swap(swap(X)) = X swap(f(X,Y)) = f(swap(X), swap(Y)) swap(x) = y swap(y) = x\n\nx:1,swap:1,y:2 ? swap(x) = y is same weight, it will prefer swaps y -> swap(x) swap_xy == weight(x) - weight(y) ? for multivariate. Analog of weight(lift) = cod(lift) - dom(lift) x > y > z > … to get sorting.\n\ne1(x,y) = e2(y) swap(e1(x,y)) = swap(e2(y)) e1(y,x) = e2(x) sure. x,y,z\n\ne2(x) = swap(e1(y,x))\n\nhmm. My previous example did loop.\n\ne at different level should never be being compared directly without l in between. And at a level it’s abritrary\n\nlpo = the worst subterm gets better (what is worst? The worst term is the top one)\n\n[https://www.jaist.ac.jp/project/maxcomp/](https://www.jaist.ac.jp/project/maxcomp/)\n\n[https://github.com/yaspar-org/semi-persistent/blob/main/egraph/doc/design/00-table-of-contents.md](https://github.com/yaspar-org/semi-persistent/blob/main/egraph/doc/design/00-table-of-contents.md)\n\nordered slotted is perhaps a mapping from infinite context e(v0, v1,v4) = [true, true, false, false, true, false, ….]\n\nA mediation between nominal and presheafy thinny stuff?\n\nordered slotted had an easier canonicalization function. We just compact them into sorted order. This is not how full slotted worked, which normalized argument by argument.\n\norigina\n\n```\n%%file /tmp/swap.p\n\ncnf(swap, axiom, swap(x) = y).\ncnf(swapy, axiom, swap(y) = x).\ncnf(swap_involution, axiom, swap(swap(X)) = X).\ncnf(swap_mul, axiom, swap(mul(X,Y)) = mul(swap(X), swap(Y))).\ncnf(swap_zero, axiom, swap(zero) = zero).\n\ncnf(mul_zero, axiom, mul(zero,zero) = zero).\ncnf(mulzero, axiom, mul(x,zero) = zero).\ncnf(mulzero, axiom, mul(y,zero) = zero).\ncnf(mulxy, axiom, mul(x,y) = mul(y,x)).\n\n% mul(swap(X), Y) = swap(mul(X, swap(Y))). That's a good point. remove swap from first and add to all other args.\nWriting /tmp/swap.p\n! metis /tmp/swap.p\n^C\n```\n\nHmm. So I can’t get kbo to work. I think slotted would want to push swap down? Maybe this is why I need e(x,y,z)?\n\nx,y high to get rid of them in redundancies? swap low? to push swap(mul) -> mul(swap, swap) ? symmettries screw you?\n\n```\n! eprover-ho -t LPO4  -s  --auto --precedence=\"y > x > mul > zero > swap\"  --print-saturated /tmp/swap.p\n% Preprocessing class: FSSSSMSSSSSNFFN.\n% Configuration: G-E--_302_C18_F1_URBAN_RG_S04BN\n% (lift_lambdas = 1, lambda_to_forall = 1,unroll_only_formulas = 1, sine = Auto)\n% No SInE strategy applied\n% Search class: FUUPS-FFSM21-SFFFFFNN\n% partial match(1): FUUPS-FFSF21-SFFFFFNN\n% Configuration: G-E--_208_C12_00_F1_SE_CS_PI_SP_PS_S5PRR_RG_S04AN\n% Presaturation interreduction done\n\n% No proof found!\n% SZS status Satisfiable\n% Processed positive unit clauses:\ncnf(i_0_9, plain, (y=swap(x))).\ncnf(i_0_13, plain, (swap(zero)=zero)).\ncnf(i_0_11, plain, (swap(swap(X1))=X1)).\ncnf(i_0_15, plain, (mul(x,zero)=zero)).\ncnf(i_0_14, plain, (mul(zero,zero)=zero)).\ncnf(i_0_19, plain, (mul(swap(X1),X2)=swap(mul(X1,swap(X2))))).\ncnf(i_0_26, plain, (mul(zero,swap(X1))=swap(mul(zero,X1)))).\n\n% Processed negative unit clauses:\n\n% Processed non-unit clauses:\n\n% Unprocessed positive unit clauses:\n\n% Unprocessed negative unit clauses:\n\n% Unprocessed non-unit clauses:\n! eprover-ho -t KBO6  -s --auto --precedence=\"mul > zero > x > y > swap\" --order-weights=\"mul:1,zero:1,swap:1,x:3,y:4\"  --print-saturated /tmp/swap.p\n% Preprocessing class: FSSSSMSSSSSNFFN.\n% Configuration: G-E--_302_C18_F1_URBAN_RG_S04BN\n% (lift_lambdas = 1, lambda_to_forall = 1,unroll_only_formulas = 1, sine = Auto)\n% No SInE strategy applied\n% Search class: FUUPS-FFSS21-SFFFFFNN\n% partial match(1): FUUPS-FFSF21-SFFFFFNN\n% Configuration: G-E--_208_C12_00_F1_SE_CS_PI_SP_PS_S5PRR_RG_S04AN\nsetting user weights\n^C\n! eprover-ho --auto --cpu-limit=1 --silent --proof-object /tmp/test.p\n% Preprocessing class: FSMSSMSSSSSNFFN.\n% Configuration: G-E--_208_C18_F1_SE_CS_SOS_SP_PS_S5PRR_RG_S04AN\n% (lift_lambdas = 1, lambda_to_forall = 1,unroll_only_formulas = 1, sine = Auto)\n% No SInE strategy applied\n% Search class: FGHSF-FFSF21-SFFFFFNN\n% Configuration: SAT001_MinMin_p005000_rr_RG\n% Presaturation interreduction done\n\n% Proof found!\n% SZS status Unsatisfiable\n% SZS output start CNFRefutation\ncnf(clause_4, negated_conjecture, (f(X2)=X2|~big_f(X1,f(X2))|X1!=g(X2)), file('/tmp/test.p', clause_4)).\ncnf(clause_6, negated_conjecture, (big_f(X1,f(X2))|f(X2)=X2|X1!=g(X2)), file('/tmp/test.p', clause_6)).\ncnf(clause_9, negated_conjecture, (big_f(h(X1,X2),f(X1))|h(X1,X2)=X2|f(X1)!=X1), file('/tmp/test.p', clause_9)).\ncnf(clause_10, negated_conjecture, (f(X1)!=X1|h(X1,X2)!=X2|~big_f(h(X1,X2),f(X1))), file('/tmp/test.p', clause_10)).\ncnf(clause_1, axiom, (X1=a|~big_f(X1,X2)), file('/tmp/test.p', clause_1)).\ncnf(clause_3, axiom, (big_f(X1,X2)|X1!=a|X2!=b), file('/tmp/test.p', clause_3)).\ncnf(c_0_6, negated_conjecture, (f(X2)=X2|~big_f(X1,f(X2))|X1!=g(X2)), inference(fof_simplification,[status(thm)],[clause_4])).\ncnf(c_0_7, negated_conjecture, (big_f(X1,f(X2))|f(X2)=X2|X1!=g(X2)), inference(fof_simplification,[status(thm)],[clause_6])).\ncnf(c_0_8, negated_conjecture, (f(X2)=X2|~big_f(X1,f(X2))|X1!=g(X2)), c_0_6).\ncnf(c_0_9, negated_conjecture, (big_f(h(X1,X2),f(X1))|h(X1,X2)=X2|f(X1)!=X1), inference(fof_simplification,[status(thm)],[clause_9])).\ncnf(c_0_10, negated_conjecture, (big_f(X1,f(X2))|f(X2)=X2|X1!=g(X2)), c_0_7).\ncnf(c_0_11, negated_conjecture, (f(X1)=X1|~big_f(g(X1),f(X1))), inference(er,[status(thm)],[c_0_8])).\ncnf(c_0_12, negated_conjecture, (f(X1)!=X1|h(X1,X2)!=X2|~big_f(h(X1,X2),f(X1))), inference(fof_simplification,[status(thm)],[clause_10])).\ncnf(c_0_13, plain, (X1=a|~big_f(X1,X2)), inference(fof_simplification,[status(thm)],[clause_1])).\ncnf(c_0_14, negated_conjecture, (big_f(h(X1,X2),f(X1))|h(X1,X2)=X2|f(X1)!=X1), c_0_9).\ncnf(c_0_15, negated_conjecture, (f(X1)=X1), inference(csr,[status(thm)],[inference(er,[status(thm)],[c_0_10]), c_0_11])).\ncnf(c_0_16, negated_conjecture, (f(X1)!=X1|h(X1,X2)!=X2|~big_f(h(X1,X2),f(X1))), c_0_12).\ncnf(c_0_17, plain, (X1=a|~big_f(X1,X2)), c_0_13).\ncnf(c_0_18, negated_conjecture, (h(X1,X2)=X2|big_f(h(X1,X2),X1)), inference(cn,[status(thm)],[inference(rw,[status(thm)],[inference(rw,[status(thm)],[c_0_14, c_0_15]), c_0_15])])).\ncnf(c_0_19, plain, (big_f(X1,X2)|X1!=a|X2!=b), inference(fof_simplification,[status(thm)],[clause_3])).\ncnf(c_0_20, negated_conjecture, (h(X1,X2)!=X2|~big_f(h(X1,X2),X1)), inference(cn,[status(thm)],[inference(rw,[status(thm)],[inference(rw,[status(thm)],[c_0_16, c_0_15]), c_0_15])])).\ncnf(c_0_21, negated_conjecture, (h(X1,X2)=a|h(X1,X2)=X2), inference(spm,[status(thm)],[c_0_17, c_0_18])).\ncnf(c_0_22, plain, (big_f(X1,X2)|X1!=a|X2!=b), c_0_19).\ncnf(c_0_23, negated_conjecture, (h(X1,X2)=a|~big_f(X2,X1)), inference(spm,[status(thm)],[c_0_20, c_0_21])).\ncnf(c_0_24, plain, (big_f(a,b)), inference(er,[status(thm)],[inference(er,[status(thm)],[c_0_22])])).\ncnf(c_0_25, negated_conjecture, (~big_f(a,X1)), inference(er,[status(thm)],[inference(spm,[status(thm)],[c_0_20, c_0_23])])).\ncnf(c_0_26, plain, ($false), inference(sr,[status(thm)],[c_0_24, c_0_25]), ['proof']).\n% SZS output end CNFRefutation\n%%file /tmp/lift.p\n\ncnf(mulname, axiom, mul(id, l(zero)) = e0).\ncnf(union, axiom, e0 = l(zero)).\ncnf(redundnant, axiom, mul(l(id), l(l(zero))) = l(l(zero))). % if you don't have \"union\" injectivity + homo will derive it.\n\ncnf(mul_id_id, axiom, mul(id,id) = e1). % x**2. no particular identities apply\n\ncnf(mul_zero_id, axiom, mul(zero,zero) = e2).\ncnf(mulzerozero, axiom, e2 = zero).\n\ncnf(mul_comm, axiom, mul(id, l(zero)) = mul(l(zero),id)).\ncnf(mul_assoc, axiom, mul(id,mul(id,l(zero))) = mul(mul(id,id),l(zero))).\n\ncnf(linj, axiom  , l(X) != l(Y) | X = Y).\n%cnf(lequivf, axiom, l(f(X)) = f(l(X))).\ncnf(mul_homo, axiom, l(mul(X,Y)) = mul(l(X),l(Y))).\nOverwriting /tmp/lift.p\n! eprover-ho --precedence=\"mul > zero > e1 > e0 > e2 > id > l\" -t LPO4  /tmp/lift.p --silent --print-oriented-eqlits-as-rules --print-saturated\n% (lift_lambdas = 1, lambda_to_forall = 1,unroll_only_formulas = 1, sine = (null))\n^C\n! eprover-ho --precedence=\"mul > zero > id > e2 > e0  > e1 > l\" -t KBO6 --order-weights=\"id:2,zero:1,l:1,e0:2,e1:2,e2:1,mul:1\" /tmp/lift.p --silent --print-oriented-eqlits-as-rules --print-saturated\n% (lift_lambdas = 1, lambda_to_forall = 1,unroll_only_formulas = 1, sine = (null))\nsetting user weights\n\n% No proof found!\n% SZS status Satisfiable\n% Processed positive unit clauses:\ncnf(i_0_16, plain, (zero->e2)).\ncnf(i_0_12, plain, (e0->l(e2))).\ncnf(i_0_14, plain, (mul(id,id)->e1)).\ncnf(i_0_15, plain, (mul(e2,e2)->e2)).\ncnf(i_0_11, plain, (mul(id,l(e2))->l(e2))).\ncnf(i_0_20, plain, (mul(l(X1),l(X2))->l(mul(X1,X2)))).\ncnf(i_0_17, plain, (mul(l(e2),id)->l(e2))).\ncnf(i_0_18, plain, (mul(e1,l(e2))->l(e2))).\n\n% Processed negative unit clauses:\n\n% Processed non-unit clauses:\ncnf(i_0_19, plain, (X1->X2|l(X1)!->l(X2))).\n\n% Unprocessed positive unit clauses:\n\n% Unprocessed negative unit clauses:\n\n% Unprocessed non-unit clauses:\n%%file /tmp/lift.p\n\ncnf(a1, axiom, l(e1) = e2).\n%cnf(a2, axiom, l(e2) = e3).\ncnf(a3, axiom, f(l(e1)) = e4).\n% l injective also\ncnf(linj, axiom  , l(X) != l(Y) | X = Y).\ncnf(lequivf, axiom, l(f(X)) = f(l(X))).\nWriting /tmp/lift.p\n! metis /tmp/lift.p\n^C\n! eprover-ho   --print-oriented-eqlits-as-rules  --term-ordering=LPO4 --precedence=\"f  > e2 > e4 > e1 >  l\" --print-saturated /tmp/lift.p\n```\n\ntree-like proof relevant partial orders. I expected to have to sometimes do LCA or traverse down the uf?\n\nunary rewriting theory combination. If I have a normalization procedure for a string system encoded as unary symbols, I don’t think I can actually hurt it by adding in new ground eqautions? Including ones.\n\nSlotted - information goes down, thinning information goes up thin is alpha euqal on the nose. slotted is disequal but g related\n\nString knuth bendix with “fat” characters coproduct knuth bendix.\n\nFrex\n\nTower of equivalances\n\nsin(pi *(?x + 1)) = cos(pi* ?x)\n\nEmatching needs an unapply interface that is group aware?\n\noffset, linear enodes, proofs need to record how ematching worked.\n\nversioned egraphs on edges?\n\nRepresentations of Relations. But we also have things on the nodes. Convert constraint graphs into constraint trees\n\nSkolems fn reify(x : FatId) -> RawId\n\nWhat the hell just happened in ordered slotted v10 = lambda *args: args[10] sin is still lifted to sin’\n\nAppliedId\n\ne12(2,4,6) f(v1,v0) is fine. Arguments are not linked to slots.\n\nUsing the opposite form of thinning. Sure. type Wide = list[int]\n\nNamed thinnings. Maybe this is a useful concept to figure out what is the relation of thinning and slotted/redundancy. A thinning is a bitvector recipe saying how to find one list as a subsequence of anothet list. A named thinning is a boolean valued struct as a recipe on how to extract a subrecord from another record (take a subset of the keys/names). The *args combinators I wrote in my blog post can be made to work on **kwargs by using named thinnings. This can all be discussed without the concept of renaming, I dunno if it is useful without renaming though.\n\nNamed thinning + permutative renamings does seem like an independent factoring of partial injective renamings.Philip Zucker: Using records for the sin cos model feels weird but is fine. Instead of R^3, use {x : R, y: R, z : R}. Its still mathematical.\n\nMy symbols were all (T -> R) -> (T -> R) lifted, but Var wasn’t\n\nId PComp(Const())\n\nMaybe PComp is fused with App. Or\n\nApp == PComp\ndef pcomp(f):\nreturn lambda g0: lmabda g1: … lambda *args:\ndef app(f, g):\nreturn lambda*gs: lambda *args: f(g(*args), *[g(*args) for g in gs])\n\nenum Node { App(Node, Node), Symbol( name : String, arity : usize) PComp //Id }\n\nVar = Symbol(“Id”, arity=1)\n\n``` php\ntype NThin = dict[str, bool]\n\ndef nthin(t : NThin, d : dict) -> dict:\n    return {k : v for k,v in d.items() if t[k]}\n\ndef nlift(t : NThin, f):\n    return lambda **kwargs: f(** nthin(t, kwargs))\n\n# oh interesting. They have their own defaults... But then lift needs to carry the default? And dump is a no-op\ndef foo(x=0, y=1):\n    return x - y\n\nnlift({\"x\": True, \"y\": True, \"z\" : False}, foo)(x=10, y=20, z=200)\n-10\nphp\ndef replace_thin(t : dict, d : dict) -> dict:\n    return {**d, **t}\ndef rlift(t, f):\n    return lambda **kwargs: f(** replace_thin(t, kwargs))\n\nrlift({\"y\": 100}, foo)(x=3)\n-97\n```\n\n", "url": "https://wpnews.pro/news/lifting-e-graphs", "canonical_source": "https://www.philipzucker.com/lifting_egraph/", "published_at": "2026-06-13 18:30:56+00:00", "updated_at": "2026-06-14 10:46:08.290150+00:00", "lang": "en", "topics": ["ai-research", "developer-tools"], "entities": ["EGRAPHS workshop", "PLDI"], "alternates": {"html": "https://wpnews.pro/news/lifting-e-graphs", "markdown": "https://wpnews.pro/news/lifting-e-graphs.md", "text": "https://wpnews.pro/news/lifting-e-graphs.txt", "jsonld": "https://wpnews.pro/news/lifting-e-graphs.jsonld"}}