Below is a small arithmetic expression. The engine knows a handful of rewrite rules — algebraic identities and strength-reduction tricks. Each Saturate step fires all applicable rules and grows the e-graph. When no new equivalences can be added the graph is saturated. Then Extract cheapest walks every e-class and picks the form with the lowest cost (operator count).
<!-- E-graph equality saturation demo -->
<p class="hint">Start with <code>(2*a)*(a/1)</code> — cost 7 operators. Fire rewrite rules to grow the equivalence set, then extract the cheapest form.</p>
<div class="expr-row">
<span class="label">Expression:</span>
<code id="current-expr" class="expr-display"></code>
</div>
<div class="equiv-box" id="equiv-box">
<div class="box-title">Equivalent forms found so far</div>
<div id="equiv-container" class="equiv-container"></div>
</div>
<div class="status-row" id="status-row"></div>
<div class="btns">
<button id="btn-step" type="button">Saturate step</button>
<button id="btn-saturate" type="button">Saturate all</button>
<button id="btn-extract" type="button">Extract cheapest</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div class="extracted-row" id="extracted-row" style="display:none">
<span class="label">Best form:</span>
<code id="extracted-expr" class="expr-display best"></code>
<span id="extracted-cost" class="cost-badge"></span>
</div>
/* Styles for the equality saturation demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.expr-row { display: flex; align-items: center; gap: .5rem; margin-bottom: .6rem; flex-wrap: wrap; }
.label { font-size: .8rem; color: #666; font-weight: 600; white-space: nowrap; }
.expr-display { background: #e8eef3; border: 1px solid #cdd9e3; border-radius: 6px;
padding: .25rem .55rem; font-size: .92rem; color: #1d3557; }
.expr-display.best { background: #d4edda; border-color: #a8d5b5; color: #155724; }
.equiv-box { border: 1.5px solid #cdd9e3; border-radius: 10px; padding: .6rem .8rem;
background: #f7fafc; margin-bottom: .65rem; min-height: 60px; }
.box-title { font-size: .72rem; font-weight: 700; color: #5a7088; text-transform: uppercase;
letter-spacing: .06em; margin-bottom: .45rem; }
.equiv-container { display: flex; flex-wrap: wrap; gap: .35rem; }
.equiv-chip { border: 1.5px solid #5a7088; border-radius: 6px; padding: .18rem .5rem;
background: #fff; font-size: .8rem; font-family: ui-monospace, monospace;
color: #1d3557; white-space: nowrap; }
.equiv-chip.new { border-color: #0a7d33; color: #0a7d33; background: #f0fff4; font-weight: 700; }
.equiv-chip.best-chip { border-color: #856404; color: #856404; background: #fff3cd; }
.status-row { font-size: .88rem; font-weight: 600; min-height: 1.4em; margin: .3rem 0; }
.status-row.ok { color: #0a7d33; }
.status-row.info { color: #1d3557; }
.status-row.done { color: #856404; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
button { font: 600 13px system-ui; padding: .38rem .82rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
.extracted-row { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.cost-badge { font-size: .78rem; background: #fff3cd; border: 1px solid #ffc107;
border-radius: 5px; padding: .15rem .45rem; color: #856404; font-weight: 700; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Equality saturation demo — BFS over equivalent expressions
// We generate all equivalent full expressions via BFS; each step fires all rules at every sub-tree position.
// Cost = total AST node count (operators + leaves)
function cost(n){
if(!n.args||n.args.length===0) return 1;
return 1 + n.args.reduce(function(s,c){ return s+cost(c); }, 0);
}
function exprStr(n){
if(n.op==='num') return String(n.val);
if(n.op==='var') return n.name;
if(n.op==='sq') return 'sq('+exprStr(n.args[0])+')';
if(n.op==='<<') return exprStr(n.args[0])+'<<'+exprStr(n.args[1]);
return '('+exprStr(n.args[0])+n.op+exprStr(n.args[1])+')';
}
function exprEq(a,b){
if(a.op!==b.op) return false;
if(a.op==='num') return a.val===b.val;
if(a.op==='var') return a.name===b.name;
if(!a.args||!b.args||a.args.length!==b.args.length) return false;
return a.args.every(function(x,i){ return exprEq(x,b.args[i]); });
}
function exprKey(n){ return exprStr(n); }
// Rewrite rules: produce all one-step variants of an expression
// Rules: x/1->x, 2*x->x<<1, commutativity, x*x->sq(x)
function oneStepVariants(expr){
var result = [];
// rule: x / 1 -> x
if(expr.op==='/'&&expr.args&&exprEq(expr.args[1],{op:'num',val:1}))
result.push(expr.args[0]);
if(expr.op==='*'&&expr.args){
// rule: 2*x -> x<<1 (strength reduction)
if(exprEq(expr.args[0],{op:'num',val:2}))
result.push({op:'<<',args:[expr.args[1],{op:'num',val:1}]});
if(exprEq(expr.args[1],{op:'num',val:2}))
result.push({op:'<<',args:[expr.args[0],{op:'num',val:1}]});
// rule: a*b -> b*a (commutativity)
result.push({op:'*',args:[expr.args[1],expr.args[0]]});
// rule: x*x -> sq(x) (squaring)
if(exprEq(expr.args[0],expr.args[1]))
result.push({op:'sq',args:[expr.args[0]]});
}
// apply rules recursively in sub-expressions and rebuild parent
if(expr.args){
for(var i=0;i<expr.args.length;i++){
var childVars = oneStepVariants(expr.args[i]);
for(var j=0;j<childVars.length;j++){
var newArgs=expr.args.slice(); newArgs[i]=childVars[j];
result.push({op:expr.op,args:newArgs});
}
}
}
return result;
}
// Initial expression: (2 * a) * (a / 1) cost = 7
var INITIAL_EXPR = { op: '*', args: [
{ op: '*', args: [{ op: 'num', val: 2 }, { op: 'var', name: 'a' }] },
{ op: '/', args: [{ op: 'var', name: 'a' }, { op: 'num', val: 1 }] }
]};
// State: all known equivalent forms + BFS frontier
var equivMap = new Map(); // key -> expression object for all known equivalents
var frontier = []; // expressions added in the last step (BFS frontier)
var saturated = false;
var stepCount = 0;
var lastNewKeys = [];
function init(){
equivMap = new Map();
frontier = [INITIAL_EXPR];
equivMap.set(exprKey(INITIAL_EXPR), INITIAL_EXPR);
saturated = false;
stepCount = 0;
lastNewKeys = [exprKey(INITIAL_EXPR)];
}
// One saturation step: expand each frontier expression by one rule application
function doStep(){
if(saturated||frontier.length===0) return false;
var nextFrontier = [];
lastNewKeys = [];
for(var fi=0;fi<frontier.length;fi++){
var variants = oneStepVariants(frontier[fi]);
for(var vi=0;vi<variants.length;vi++){
var k = exprKey(variants[vi]);
if(!equivMap.has(k)){
equivMap.set(k, variants[vi]);
nextFrontier.push(variants[vi]);
lastNewKeys.push(k);
}
}
}
frontier = nextFrontier;
if(frontier.length===0) saturated=true;
return lastNewKeys.length>0;
}
// Extract: find the minimum-cost expression across all equivalents
function extractBest(){
var best=null, bestCost=Infinity;
equivMap.forEach(function(expr){
var c=cost(expr);
if(c<bestCost){ bestCost=c; best=expr; }
});
return {expr:best, cost:bestCost};
}
function renderEquiv(highlightBest){
var cont=document.getElementById('equiv-container');
cont.innerHTML='';
var bestKey=null;
if(highlightBest){
var b=extractBest(); if(b.expr) bestKey=exprKey(b.expr);
}
equivMap.forEach(function(expr,k){
var chip=document.createElement('span');
var isNew=lastNewKeys.indexOf(k)>=0;
var isBest=highlightBest&&k===bestKey;
chip.className='equiv-chip'+(isNew?' new':'')+(isBest?' best-chip':'');
chip.textContent=exprStr(expr);
cont.appendChild(chip);
});
}
function setStatus(msg,cls){
var s=document.getElementById('status-row');
s.textContent=msg; s.className='status-row '+(cls||'');
}
function reset(){
init();
document.getElementById('current-expr').textContent=exprStr(INITIAL_EXPR);
document.getElementById('extracted-row').style.display='none';
setStatus('Ready. Press ’Saturate step’ to fire rewrite rules.','info');
renderEquiv(false);
document.getElementById('btn-step').disabled=false;
document.getElementById('btn-saturate').disabled=false;
}
document.getElementById('btn-step').addEventListener('click',function(){
if(saturated){ setStatus('Already saturated — no new equivalences possible.','done'); return; }
stepCount++;
doStep();
renderEquiv(false);
if(saturated) setStatus('Saturated! All equivalent forms found.','done');
else setStatus(fmt('Step %stepCount% — %size% equivalent forms so far.', { stepCount: stepCount, size: equivMap.size }),'ok');
});
document.getElementById('btn-saturate').addEventListener('click',function(){
if(saturated){ setStatus('Already saturated — no new equivalences possible.','done'); return; }
while(!saturated&&stepCount<20){ stepCount++; doStep(); }
renderEquiv(false);
setStatus(fmt('Saturated! Found %size% equivalent forms. Now extract the cheapest.', { size: equivMap.size }),'done');
});
document.getElementById('btn-extract').addEventListener('click',function(){
var result=extractBest();
if(!result.expr){ setStatus('No expressions yet — reset first.',''); return; }
document.getElementById('extracted-expr').textContent=exprStr(result.expr);
document.getElementById('extracted-cost').textContent=fmt('cost %cost%', { cost: result.cost });
document.getElementById('extracted-row').style.display='flex';
renderEquiv(true);
setStatus(fmt('Extracted cheapest form: cost %cost% (original was %cost2%). Highlighted in orange.', { cost: result.cost, cost2: cost(INITIAL_EXPR) }),'ok');
});
document.getElementById('btn-reset').addEventListener('click', reset);
reset();
<!-- E-graph equality saturation demo -->
<p class="hint">Start with <code>(2*a)*(a/1)</code> — cost 7 operators. Fire rewrite rules to grow the equivalence set, then extract the cheapest form.</p>
<div class="expr-row">
<span class="label">Expression:</span>
<code id="current-expr" class="expr-display"></code>
</div>
<div class="equiv-box" id="equiv-box">
<div class="box-title">Equivalent forms found so far</div>
<div id="equiv-container" class="equiv-container"></div>
</div>
<div class="status-row" id="status-row"></div>
<div class="btns">
<button id="btn-step" type="button">Saturate step</button>
<button id="btn-saturate" type="button">Saturate all</button>
<button id="btn-extract" type="button">Extract cheapest</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div class="extracted-row" id="extracted-row" style="display:none">
<span class="label">Best form:</span>
<code id="extracted-expr" class="expr-display best"></code>
<span id="extracted-cost" class="cost-badge"></span>
</div>
/* Styles for the equality saturation demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.expr-row { display: flex; align-items: center; gap: .5rem; margin-bottom: .6rem; flex-wrap: wrap; }
.label { font-size: .8rem; color: #666; font-weight: 600; white-space: nowrap; }
.expr-display { background: #e8eef3; border: 1px solid #cdd9e3; border-radius: 6px;
padding: .25rem .55rem; font-size: .92rem; color: #1d3557; }
.expr-display.best { background: #d4edda; border-color: #a8d5b5; color: #155724; }
.equiv-box { border: 1.5px solid #cdd9e3; border-radius: 10px; padding: .6rem .8rem;
background: #f7fafc; margin-bottom: .65rem; min-height: 60px; }
.box-title { font-size: .72rem; font-weight: 700; color: #5a7088; text-transform: uppercase;
letter-spacing: .06em; margin-bottom: .45rem; }
.equiv-container { display: flex; flex-wrap: wrap; gap: .35rem; }
.equiv-chip { border: 1.5px solid #5a7088; border-radius: 6px; padding: .18rem .5rem;
background: #fff; font-size: .8rem; font-family: ui-monospace, monospace;
color: #1d3557; white-space: nowrap; }
.equiv-chip.new { border-color: #0a7d33; color: #0a7d33; background: #f0fff4; font-weight: 700; }
.equiv-chip.best-chip { border-color: #856404; color: #856404; background: #fff3cd; }
.status-row { font-size: .88rem; font-weight: 600; min-height: 1.4em; margin: .3rem 0; }
.status-row.ok { color: #0a7d33; }
.status-row.info { color: #1d3557; }
.status-row.done { color: #856404; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
button { font: 600 13px system-ui; padding: .38rem .82rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
.extracted-row { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.cost-badge { font-size: .78rem; background: #fff3cd; border: 1px solid #ffc107;
border-radius: 5px; padding: .15rem .45rem; color: #856404; font-weight: 700; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Equality saturation demo — BFS over equivalent expressions
// We generate all equivalent full expressions via BFS; each step fires all rules at every sub-tree position.
// Cost = total AST node count (operators + leaves)
function cost(n){
if(!n.args||n.args.length===0) return 1;
return 1 + n.args.reduce(function(s,c){ return s+cost(c); }, 0);
}
function exprStr(n){
if(n.op==='num') return String(n.val);
if(n.op==='var') return n.name;
if(n.op==='sq') return 'sq('+exprStr(n.args[0])+')';
if(n.op==='<<') return exprStr(n.args[0])+'<<'+exprStr(n.args[1]);
return '('+exprStr(n.args[0])+n.op+exprStr(n.args[1])+')';
}
function exprEq(a,b){
if(a.op!==b.op) return false;
if(a.op==='num') return a.val===b.val;
if(a.op==='var') return a.name===b.name;
if(!a.args||!b.args||a.args.length!==b.args.length) return false;
return a.args.every(function(x,i){ return exprEq(x,b.args[i]); });
}
function exprKey(n){ return exprStr(n); }
// Rewrite rules: produce all one-step variants of an expression
// Rules: x/1->x, 2*x->x<<1, commutativity, x*x->sq(x)
function oneStepVariants(expr){
var result = [];
// rule: x / 1 -> x
if(expr.op==='/'&&expr.args&&exprEq(expr.args[1],{op:'num',val:1}))
result.push(expr.args[0]);
if(expr.op==='*'&&expr.args){
// rule: 2*x -> x<<1 (strength reduction)
if(exprEq(expr.args[0],{op:'num',val:2}))
result.push({op:'<<',args:[expr.args[1],{op:'num',val:1}]});
if(exprEq(expr.args[1],{op:'num',val:2}))
result.push({op:'<<',args:[expr.args[0],{op:'num',val:1}]});
// rule: a*b -> b*a (commutativity)
result.push({op:'*',args:[expr.args[1],expr.args[0]]});
// rule: x*x -> sq(x) (squaring)
if(exprEq(expr.args[0],expr.args[1]))
result.push({op:'sq',args:[expr.args[0]]});
}
// apply rules recursively in sub-expressions and rebuild parent
if(expr.args){
for(var i=0;i<expr.args.length;i++){
var childVars = oneStepVariants(expr.args[i]);
for(var j=0;j<childVars.length;j++){
var newArgs=expr.args.slice(); newArgs[i]=childVars[j];
result.push({op:expr.op,args:newArgs});
}
}
}
return result;
}
// Initial expression: (2 * a) * (a / 1) cost = 7
var INITIAL_EXPR = { op: '*', args: [
{ op: '*', args: [{ op: 'num', val: 2 }, { op: 'var', name: 'a' }] },
{ op: '/', args: [{ op: 'var', name: 'a' }, { op: 'num', val: 1 }] }
]};
// State: all known equivalent forms + BFS frontier
var equivMap = new Map(); // key -> expression object for all known equivalents
var frontier = []; // expressions added in the last step (BFS frontier)
var saturated = false;
var stepCount = 0;
var lastNewKeys = [];
function init(){
equivMap = new Map();
frontier = [INITIAL_EXPR];
equivMap.set(exprKey(INITIAL_EXPR), INITIAL_EXPR);
saturated = false;
stepCount = 0;
lastNewKeys = [exprKey(INITIAL_EXPR)];
}
// One saturation step: expand each frontier expression by one rule application
function doStep(){
if(saturated||frontier.length===0) return false;
var nextFrontier = [];
lastNewKeys = [];
for(var fi=0;fi<frontier.length;fi++){
var variants = oneStepVariants(frontier[fi]);
for(var vi=0;vi<variants.length;vi++){
var k = exprKey(variants[vi]);
if(!equivMap.has(k)){
equivMap.set(k, variants[vi]);
nextFrontier.push(variants[vi]);
lastNewKeys.push(k);
}
}
}
frontier = nextFrontier;
if(frontier.length===0) saturated=true;
return lastNewKeys.length>0;
}
// Extract: find the minimum-cost expression across all equivalents
function extractBest(){
var best=null, bestCost=Infinity;
equivMap.forEach(function(expr){
var c=cost(expr);
if(c<bestCost){ bestCost=c; best=expr; }
});
return {expr:best, cost:bestCost};
}
function renderEquiv(highlightBest){
var cont=document.getElementById('equiv-container');
cont.innerHTML='';
var bestKey=null;
if(highlightBest){
var b=extractBest(); if(b.expr) bestKey=exprKey(b.expr);
}
equivMap.forEach(function(expr,k){
var chip=document.createElement('span');
var isNew=lastNewKeys.indexOf(k)>=0;
var isBest=highlightBest&&k===bestKey;
chip.className='equiv-chip'+(isNew?' new':'')+(isBest?' best-chip':'');
chip.textContent=exprStr(expr);
cont.appendChild(chip);
});
}
function setStatus(msg,cls){
var s=document.getElementById('status-row');
s.textContent=msg; s.className='status-row '+(cls||'');
}
function reset(){
init();
document.getElementById('current-expr').textContent=exprStr(INITIAL_EXPR);
document.getElementById('extracted-row').style.display='none';
setStatus('Ready. Press ’Saturate step’ to fire rewrite rules.','info');
renderEquiv(false);
document.getElementById('btn-step').disabled=false;
document.getElementById('btn-saturate').disabled=false;
}
document.getElementById('btn-step').addEventListener('click',function(){
if(saturated){ setStatus('Already saturated — no new equivalences possible.','done'); return; }
stepCount++;
doStep();
renderEquiv(false);
if(saturated) setStatus('Saturated! All equivalent forms found.','done');
else setStatus(fmt('Step %stepCount% — %size% equivalent forms so far.', { stepCount: stepCount, size: equivMap.size }),'ok');
});
document.getElementById('btn-saturate').addEventListener('click',function(){
if(saturated){ setStatus('Already saturated — no new equivalences possible.','done'); return; }
while(!saturated&&stepCount<20){ stepCount++; doStep(); }
renderEquiv(false);
setStatus(fmt('Saturated! Found %size% equivalent forms. Now extract the cheapest.', { size: equivMap.size }),'done');
});
document.getElementById('btn-extract').addEventListener('click',function(){
var result=extractBest();
if(!result.expr){ setStatus('No expressions yet — reset first.',''); return; }
document.getElementById('extracted-expr').textContent=exprStr(result.expr);
document.getElementById('extracted-cost').textContent=fmt('cost %cost%', { cost: result.cost });
document.getElementById('extracted-row').style.display='flex';
renderEquiv(true);
setStatus(fmt('Extracted cheapest form: cost %cost% (original was %cost2%). Highlighted in orange.', { cost: result.cost, cost2: cost(INITIAL_EXPR) }),'ok');
});
document.getElementById('btn-reset').addEventListener('click', reset);
reset();
Comments
Loading comments...