Below is a tiny heap. The ROOT is what your program can directly see — local variables and globals. Arrows are references: object A keeps object B alive only if there's a path of arrows from the root to B.
<p class="hint">The <b>ROOT</b> is what your program can see directly. Arrows are references. Click an arrow to cut it (reassign a variable), then run the collector: it <b>marks</b> everything reachable from the root and <b>sweeps</b> the rest.</p>
<svg id="heap" viewBox="0 0 420 240" class="heap"></svg>
<div class="status" id="status">Cut an arrow, then mark & sweep.</div>
<div class="btns">
<button id="run" type="button">Mark & Sweep</button>
<button id="reset" type="button" class="ghost">Reset graph</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.heap { width: 100%; height: auto; background: #f4f7fa; border-radius: 10px; }
.node circle { stroke-width: 2; transition: all .25s; }
.node text { font: 700 13px ui-monospace, monospace; fill: #fff; pointer-events: none; }
.node.obj circle { fill: #6c7a89; stroke: #55626f; }
.node.root circle { fill: #1d3557; stroke: #142844; }
.node.marked circle { fill: #0a7d33; stroke: #075c25; }
.node.swept circle { fill: #e6e9ec; stroke: #c3c9cf; }
.node.swept text { fill: #aeb6bd; }
.edge { stroke: #8a98a6; stroke-width: 2.4; cursor: pointer; transition: stroke .15s; }
.edge:hover { stroke: #e63946; stroke-width: 3.4; }
.edge.cut { display: none; }
.arrowhead { fill: #8a98a6; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.info { color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// A tracing mark-and-sweep garbage collector over a tiny object graph.
// Nodes are heap objects; "root" is the program's entry point. An object is
// LIVE iff some path of (uncut) reference arrows reaches it from the root.
// mark() does a graph traversal (BFS) from the root; sweep() frees the rest.
// This is the McCarthy (1960) idea: reachability decides what survives.
const NODES = {
root: { x: 60, y: 120, label: 'ROOT', root: true },
a: { x: 170, y: 60, label: 'A' },
b: { x: 170, y: 180, label: 'B' },
c: { x: 290, y: 60, label: 'C' },
d: { x: 290, y: 180, label: 'D' },
e: { x: 390, y: 120, label: 'E' },
};
// Directed references. Cut any of these to drop a reference.
const BASE_EDGES = [
['root','a'], ['root','b'], ['a','c'], ['b','d'], ['c','e'], ['d','e'], ['c','d'],
];
let edges = BASE_EDGES.map(e => ({ from: e[0], to: e[1], cut: false }));
let marked = new Set();
let swept = false;
// Reachable set from root following only uncut edges (BFS).
function reachable() {
const adj = {}; for (const k in NODES) adj[k] = [];
edges.forEach(e => { if (!e.cut) adj[e.from].push(e.to); });
const seen = new Set(['root']); const q = ['root'];
while (q.length) { const n = q.shift();
for (const m of adj[n]) if (!seen.has(m)) { seen.add(m); q.push(m); } }
return seen;
}
function draw() {
const svg = document.getElementById('heap');
svg.innerHTML =
'<defs><marker id="ah" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto">' +
'<path class="arrowhead" d="M0,0 L7,3 L0,6 z"/></marker></defs>';
// edges first
edges.forEach((e, i) => {
const A = NODES[e.from], B = NODES[e.to];
const dx = B.x - A.x, dy = B.y - A.y, len = Math.hypot(dx, dy);
const ux = dx / len, uy = dy / len, r = 22;
const x1 = A.x + ux * r, y1 = A.y + uy * r;
const x2 = B.x - ux * (r + 8), y2 = B.y - uy * (r + 8);
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', x1); line.setAttribute('y1', y1);
line.setAttribute('x2', x2); line.setAttribute('y2', y2);
line.setAttribute('marker-end', 'url(#ah)');
line.setAttribute('class', 'edge' + (e.cut ? ' cut' : ''));
line.addEventListener('click', () => { e.cut = !e.cut; swept = false; marked = new Set();
setStatus('Reference cut. Now run Mark & Sweep.', 'info'); draw(); });
svg.appendChild(line);
});
// nodes
for (const k in NODES) {
const n = NODES[k];
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
let cls = 'node ' + (n.root ? 'root' : 'obj');
if (swept) { if (marked.has(k)) cls = 'node marked'; else if (!n.root) cls = 'node swept'; }
g.setAttribute('class', cls);
const c = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
c.setAttribute('cx', n.x); c.setAttribute('cy', n.y); c.setAttribute('r', 22);
const t = document.createElementNS('http://www.w3.org/2000/svg', 'text');
t.setAttribute('x', n.x); t.setAttribute('y', n.y + 4);
t.setAttribute('text-anchor', 'middle'); t.textContent = n.label;
g.appendChild(c); g.appendChild(t); svg.appendChild(g);
}
}
function setStatus(t, cls) { const s = document.getElementById('status'); s.textContent = t; s.className = 'status ' + cls; }
function run() {
marked = reachable(); swept = true;
const freed = Object.keys(NODES).filter(k => !NODES[k].root && !marked.has(k));
draw();
const live = [...marked].filter(k => !NODES[k].root).map(k => NODES[k].label);
if (freed.length === 0) setStatus(fmt('Marked %length% live object(s): %live%. Nothing to free.', { length: live.length, live: live.join(', ') }), 'ok');
else setStatus(fmt('Swept %length% object(s): %map%. Live: %v%.', { length: freed.length, map: freed.map(k => NODES[k].label).join(', '), v: live.join(', ') || 'none' }), 'ok');
console.log('reachable:', [...marked].join(','), '| freed:', freed.join(',') || 'none');
}
function reset() {
edges = BASE_EDGES.map(e => ({ from: e[0], to: e[1], cut: false }));
marked = new Set(); swept = false; setStatus('Cut an arrow, then mark & sweep.', 'info'); draw();
}
document.getElementById('run').addEventListener('click', run);
document.getElementById('reset').addEventListener('click', reset);
draw();
<p class="hint">The <b>ROOT</b> is what your program can see directly. Arrows are references. Click an arrow to cut it (reassign a variable), then run the collector: it <b>marks</b> everything reachable from the root and <b>sweeps</b> the rest.</p>
<svg id="heap" viewBox="0 0 420 240" class="heap"></svg>
<div class="status" id="status">Cut an arrow, then mark & sweep.</div>
<div class="btns">
<button id="run" type="button">Mark & Sweep</button>
<button id="reset" type="button" class="ghost">Reset graph</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.heap { width: 100%; height: auto; background: #f4f7fa; border-radius: 10px; }
.node circle { stroke-width: 2; transition: all .25s; }
.node text { font: 700 13px ui-monospace, monospace; fill: #fff; pointer-events: none; }
.node.obj circle { fill: #6c7a89; stroke: #55626f; }
.node.root circle { fill: #1d3557; stroke: #142844; }
.node.marked circle { fill: #0a7d33; stroke: #075c25; }
.node.swept circle { fill: #e6e9ec; stroke: #c3c9cf; }
.node.swept text { fill: #aeb6bd; }
.edge { stroke: #8a98a6; stroke-width: 2.4; cursor: pointer; transition: stroke .15s; }
.edge:hover { stroke: #e63946; stroke-width: 3.4; }
.edge.cut { display: none; }
.arrowhead { fill: #8a98a6; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.info { color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// A tracing mark-and-sweep garbage collector over a tiny object graph.
// Nodes are heap objects; "root" is the program's entry point. An object is
// LIVE iff some path of (uncut) reference arrows reaches it from the root.
// mark() does a graph traversal (BFS) from the root; sweep() frees the rest.
// This is the McCarthy (1960) idea: reachability decides what survives.
const NODES = {
root: { x: 60, y: 120, label: 'ROOT', root: true },
a: { x: 170, y: 60, label: 'A' },
b: { x: 170, y: 180, label: 'B' },
c: { x: 290, y: 60, label: 'C' },
d: { x: 290, y: 180, label: 'D' },
e: { x: 390, y: 120, label: 'E' },
};
// Directed references. Cut any of these to drop a reference.
const BASE_EDGES = [
['root','a'], ['root','b'], ['a','c'], ['b','d'], ['c','e'], ['d','e'], ['c','d'],
];
let edges = BASE_EDGES.map(e => ({ from: e[0], to: e[1], cut: false }));
let marked = new Set();
let swept = false;
// Reachable set from root following only uncut edges (BFS).
function reachable() {
const adj = {}; for (const k in NODES) adj[k] = [];
edges.forEach(e => { if (!e.cut) adj[e.from].push(e.to); });
const seen = new Set(['root']); const q = ['root'];
while (q.length) { const n = q.shift();
for (const m of adj[n]) if (!seen.has(m)) { seen.add(m); q.push(m); } }
return seen;
}
function draw() {
const svg = document.getElementById('heap');
svg.innerHTML =
'<defs><marker id="ah" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto">' +
'<path class="arrowhead" d="M0,0 L7,3 L0,6 z"/></marker></defs>';
// edges first
edges.forEach((e, i) => {
const A = NODES[e.from], B = NODES[e.to];
const dx = B.x - A.x, dy = B.y - A.y, len = Math.hypot(dx, dy);
const ux = dx / len, uy = dy / len, r = 22;
const x1 = A.x + ux * r, y1 = A.y + uy * r;
const x2 = B.x - ux * (r + 8), y2 = B.y - uy * (r + 8);
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', x1); line.setAttribute('y1', y1);
line.setAttribute('x2', x2); line.setAttribute('y2', y2);
line.setAttribute('marker-end', 'url(#ah)');
line.setAttribute('class', 'edge' + (e.cut ? ' cut' : ''));
line.addEventListener('click', () => { e.cut = !e.cut; swept = false; marked = new Set();
setStatus('Reference cut. Now run Mark & Sweep.', 'info'); draw(); });
svg.appendChild(line);
});
// nodes
for (const k in NODES) {
const n = NODES[k];
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
let cls = 'node ' + (n.root ? 'root' : 'obj');
if (swept) { if (marked.has(k)) cls = 'node marked'; else if (!n.root) cls = 'node swept'; }
g.setAttribute('class', cls);
const c = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
c.setAttribute('cx', n.x); c.setAttribute('cy', n.y); c.setAttribute('r', 22);
const t = document.createElementNS('http://www.w3.org/2000/svg', 'text');
t.setAttribute('x', n.x); t.setAttribute('y', n.y + 4);
t.setAttribute('text-anchor', 'middle'); t.textContent = n.label;
g.appendChild(c); g.appendChild(t); svg.appendChild(g);
}
}
function setStatus(t, cls) { const s = document.getElementById('status'); s.textContent = t; s.className = 'status ' + cls; }
function run() {
marked = reachable(); swept = true;
const freed = Object.keys(NODES).filter(k => !NODES[k].root && !marked.has(k));
draw();
const live = [...marked].filter(k => !NODES[k].root).map(k => NODES[k].label);
if (freed.length === 0) setStatus(fmt('Marked %length% live object(s): %live%. Nothing to free.', { length: live.length, live: live.join(', ') }), 'ok');
else setStatus(fmt('Swept %length% object(s): %map%. Live: %v%.', { length: freed.length, map: freed.map(k => NODES[k].label).join(', '), v: live.join(', ') || 'none' }), 'ok');
console.log('reachable:', [...marked].join(','), '| freed:', freed.join(',') || 'none');
}
function reset() {
edges = BASE_EDGES.map(e => ({ from: e[0], to: e[1], cut: false }));
marked = new Set(); swept = false; setStatus('Cut an arrow, then mark & sweep.', 'info'); draw();
}
document.getElementById('run').addEventListener('click', run);
document.getElementById('reset').addEventListener('click', reset);
draw();
Comments
Loading comments...