Skip to content

Commit b98d97e

Browse files
Eric Prud'hommeauxclaude
andcommitted
+ SPARQL-explorer.html + algae3 --serve: web step-through of query evaluation
Generic successor to the hand-coded rule-explorer: instead of client-side reimplementation of one query's levels, the page drives the Algae 3 debugger over HTTP and builds its interface from whatever pipeline the server reports. - bin/algae3 --serve PORT [--serve-dir DIR] (bin/algae3serve.inc): the REPL debugger as a minimal JSON service - /load (script, optional mode override), /step, /run, /reset, /mode, /try (speculative pattern match), /state; result set as vars/rows/per-row proof sets; handles curl's Expect: 100-continue; CORS-open for file:// use. - SPARQL-explorer.html (self-contained, no CDN deps): pipeline panel with pretty-printed action algebra, click-to-run-to, step/run/reset, live result-set table with row-count deltas and optional proof rows, a try(...) box for speculative negation probes, side-by-side source SPARQL pane (auto-fetches the paired .rq for *.a3 URLs), and a 'Compare modes' button that runs the whole pipeline top-down AND bottom-up and diffs the final result sets - the nested-optional and negation-scoped divergences render as DIVERGENT side-by-side tables. Verified against tests/Algae3: rule-engine-dnf.a3 steps 0..3 grow the result set 1 row/0 vars -> 2 rows/11 vars -> final projection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 5127ad0 commit b98d97e

3 files changed

Lines changed: 585 additions & 0 deletions

File tree

‎SPARQL-explorer.html‎

Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
<title>SPARQL / Algae 3 Evaluation Explorer</title>
7+
<style>
8+
:root { font-family: system-ui, sans-serif; font-size: 14px;
9+
--now: #fff8e1; --done: #e8f5e9; --pend: #fafafa; }
10+
* { box-sizing: border-box; margin: 0; }
11+
body { max-width: 1500px; margin: 0 auto; padding: 16px; }
12+
h1 { font-size: 1.3em; margin-bottom: 4px; }
13+
h2 { font-size: 1.02em; border-bottom: 1px solid #ddd; padding-bottom: 3px; margin: 14px 0 8px; }
14+
p.desc { font-size: 12px; color: #555; margin: 3px 0 8px; }
15+
code { font-family: monospace; background: #f3f3f3; padding: 1px 3px; border-radius: 2px; }
16+
button { padding: 3px 12px; font-size: 12px; cursor: pointer; border: 1px solid #bbb;
17+
border-radius: 3px; background: #f4f4f4; }
18+
button:hover { background: #e8e8e8; }
19+
button.primary { background: #1565c0; border-color: #1565c0; color: white; }
20+
button.primary:hover { background: #0d47a1; }
21+
.row { display: flex; gap: 8px; align-items: center; margin: 6px 0; }
22+
.row label { font-size: 12px; color: #666; min-width: 72px; }
23+
.row input[type=text] { flex: 1; border: 1px solid #ccc; padding: 3px 7px;
24+
font-family: monospace; font-size: 12px; border-radius: 3px; }
25+
textarea { width: 100%; font-family: monospace; font-size: 11px; border: 1px solid #ccc;
26+
border-radius: 3px; padding: 6px; }
27+
#status { font-style: italic; color: #888; font-size: 12px; }
28+
#status.err { color: #c62828; }
29+
30+
.panes { display: flex; gap: 12px; align-items: flex-start; }
31+
.panes > div { flex: 1; min-width: 0; }
32+
33+
/* pipeline */
34+
#pipeline { font-family: monospace; font-size: 11px; }
35+
.act { display: flex; gap: 7px; padding: 4px 7px; border-left: 3px solid #ccc;
36+
margin: 2px 0; background: var(--pend); cursor: pointer; border-radius: 0 3px 3px 0; }
37+
.act:hover { filter: brightness(.97); }
38+
.act.done { background: var(--done); border-left-color: #388e3c; }
39+
.act.now { background: var(--now); border-left-color: #f9a825; font-weight: bold; }
40+
.act .ord { color: #999; min-width: 18px; text-align: right; }
41+
.act .alg { white-space: pre-wrap; word-break: break-all; }
42+
43+
/* results */
44+
table.sp { border-collapse: collapse; font-family: monospace; font-size: 12px; margin: 6px 0; }
45+
table.sp th, table.sp td { border: 1px solid #ccc; padding: 3px 7px; white-space: nowrap; }
46+
table.sp th { background: #e8e8f0; position: sticky; top: 0; }
47+
table.sp td.nil { color: #bbb; }
48+
#rswrap { max-height: 420px; overflow: auto; border: 1px solid #eee; }
49+
.proofrow td { background: #f6f8fa; color: #555; font-size: 10px; }
50+
.delta { font-size: 12px; color: #666; margin: 4px 0; }
51+
.delta b.grow { color: #2e7d32; } .delta b.shrink { color: #c62828; }
52+
53+
.cmp { display: flex; gap: 12px; }
54+
.cmp > div { flex: 1; overflow: auto; }
55+
details summary { cursor: pointer; font-size: 12px; color: #0366d6; }
56+
</style>
57+
</head>
58+
<body>
59+
<h1>SPARQL / Algae 3 Evaluation Explorer</h1>
60+
<p class="desc">
61+
A generic step-through of query evaluation, driven live by the
62+
<code>bin/algae3 --serve</code> debugger (see the SWObjects checkout).
63+
Load an Algae 3 pipeline — typically a SPARQL query compiled per
64+
<code>doc/sparql-to-algae3.md</code> — and advance action by action, watching
65+
the result set evolve through each triple constraint, join, filter, scope
66+
(subselect), <code>collect … by</code> (GROUP BY / aggregates) and standalone
67+
<code>{…}</code> (HAVING). Paste the source SPARQL alongside for reference.
68+
Start the server with e.g.
69+
<code>bin/algae3 --serve 8765 --serve-dir tests/Algae3</code>.
70+
</p>
71+
72+
<div class="row">
73+
<label for="server">server</label>
74+
<input type="text" id="server" value="http://localhost:8765">
75+
<label for="scriptURL">script URL</label>
76+
<input type="text" id="scriptURL" placeholder="e.g. tests/Algae3/rule-engine-dnf.a3 (fetched by the browser)">
77+
<button onclick="loadScriptURL()">Fetch</button>
78+
</div>
79+
80+
<div class="panes">
81+
<div>
82+
<h2>Algae 3 pipeline (evaluated)</h2>
83+
<textarea id="script" rows="14" placeholder="# paste an Algae 3 script (load / ask / collect ...)"></textarea>
84+
</div>
85+
<div>
86+
<h2>Source SPARQL (reference only)</h2>
87+
<textarea id="sparql" rows="14" placeholder="# optionally paste the SPARQL this pipeline compiles"></textarea>
88+
</div>
89+
</div>
90+
91+
<div class="row" style="margin-top:8px">
92+
<button class="primary" onclick="doLoad()">Load into debugger</button>
93+
<label style="min-width:0">mode</label>
94+
<select id="mode">
95+
<option value="">script's require</option>
96+
<option value="topdown">topdown (correlated)</option>
97+
<option value="bottomup">bottomup (SPARQL algebra)</option>
98+
</select>
99+
<button onclick="doStep()">Step ▸</button>
100+
<button onclick="doRun()">Run ▸▸</button>
101+
<button onclick="doReset()">Reset ⟲</button>
102+
<button onclick="compareModes()" title="Run the whole pipeline in both modes and diff the final result sets">Compare modes ⇄</button>
103+
<span id="status">idle — load a script</span>
104+
</div>
105+
106+
<div class="panes">
107+
<div style="flex:0 0 46%">
108+
<h2>Pipeline <span id="modeTag" style="font-weight:normal;font-size:11px;color:#888"></span></h2>
109+
<p class="desc">Click an action to run from the start up to (and including) it.</p>
110+
<div id="pipeline"><em style="font-size:12px;color:#999">no script loaded</em></div>
111+
<h2>Speculative match (<code>try</code>)</h2>
112+
<p class="desc">Evaluate a pattern against the current result set in the
113+
current mode <em>without committing</em> — e.g. paste a negation to see
114+
whether correlated <code>!</code> vs bottom-up Minus would drop rows.</p>
115+
<textarea id="trybox" rows="3" placeholder="!( ?s <p> ?o )"></textarea>
116+
<div class="row"><button onclick="doTry()">try ( … )</button>
117+
<span id="tryout" style="font-size:12px;color:#666"></span></div>
118+
</div>
119+
<div>
120+
<h2>Result set <span id="rowcount" style="font-weight:normal;font-size:11px;color:#888"></span></h2>
121+
<div class="delta" id="delta"></div>
122+
<label style="font-size:12px"><input type="checkbox" id="showProofs" onchange="render()"> show proof sets</label>
123+
<div id="rswrap"></div>
124+
<div id="extras" style="font-size:12px;color:#555;margin-top:6px"></div>
125+
<div id="compare"></div>
126+
</div>
127+
</div>
128+
129+
<script>
130+
"use strict";
131+
let state = null; // last server state
132+
let prevRowCount = null; // for the delta line
133+
134+
const $ = id => document.getElementById(id);
135+
const esc = s => String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;");
136+
const srv = () => $("server").value.replace(/\/+$/, "");
137+
138+
function setStatus(msg, err) {
139+
$("status").textContent = msg;
140+
$("status").className = err ? "err" : "";
141+
}
142+
143+
async function api(path, body) {
144+
const r = await fetch(srv() + path, { method: "POST", body: body ?? "" });
145+
if (!r.ok) throw new Error("HTTP " + r.status);
146+
return r.json();
147+
}
148+
149+
/* ── actions ────────────────────────────────────────────────────────────── */
150+
window.loadScriptURL = async () => {
151+
const u = $("scriptURL").value.trim();
152+
if (!u) return;
153+
try {
154+
$("script").value = await (await fetch(u)).text();
155+
// convention: a sibling .rq with the same stem is the SPARQL source
156+
const rq = u.replace(/(-topdown|-bottomup)?\.a3$/, ".rq");
157+
if (rq !== u)
158+
try { const r = await fetch(rq); if (r.ok) $("sparql").value = await r.text(); }
159+
catch (e) { /* no paired SPARQL; fine */ }
160+
setStatus("fetched " + u + " — Load into debugger to begin");
161+
} catch (e) { setStatus("fetch failed: " + e.message, true); }
162+
};
163+
164+
window.doLoad = async () => {
165+
const script = $("script").value;
166+
if (!script.trim()) { setStatus("nothing to load", true); return; }
167+
try {
168+
prevRowCount = null;
169+
state = await api("/load?mode=" + $("mode").value, script);
170+
$("compare").innerHTML = "";
171+
render();
172+
setStatus(state.error ? "load error: " + state.error
173+
: "loaded " + state.actions.length + " actions", !!state.error);
174+
} catch (e) { setStatus("server error: " + e.message + " — is bin/algae3 --serve running?", true); }
175+
};
176+
177+
window.doStep = () => advance("/step");
178+
window.doRun = () => advance("/run");
179+
window.doReset = () => { prevRowCount = null; return advance("/reset"); };
180+
181+
async function advance(path) {
182+
if (!state) { setStatus("load a script first", true); return; }
183+
try {
184+
prevRowCount = state.rows.length;
185+
state = await api(path);
186+
render();
187+
setStatus(state.error ? "action error: " + state.error
188+
: state.done ? "pipeline complete"
189+
: "at action " + state.pos + " of " + state.actions.length, !!state.error);
190+
} catch (e) { setStatus("server error: " + e.message, true); }
191+
}
192+
193+
/* click an action ordinal: reset then step to it */
194+
window.runTo = async n => {
195+
if (!state) return;
196+
try {
197+
await api("/reset");
198+
prevRowCount = null;
199+
for (let i = 0; i <= n; ++i) { prevRowCount = state.rows.length; state = await api("/step"); }
200+
render();
201+
setStatus("ran to action " + n + " (" + state.rows.length + " rows)");
202+
} catch (e) { setStatus("server error: " + e.message, true); }
203+
};
204+
205+
window.doTry = async () => {
206+
const t = $("trybox").value.trim();
207+
if (!t || !state) return;
208+
try {
209+
const r = await api("/try", t);
210+
$("tryout").textContent = r.error ? "parse error: " + r.error
211+
: `over ${r.rowsNow} rows: +${r.extended} extended, −${r.dropped} dropped (discarded)`;
212+
} catch (e) { $("tryout").textContent = "server error: " + e.message; }
213+
};
214+
215+
/* run the whole pipeline under both modes; show final tables side by side */
216+
window.compareModes = async () => {
217+
const script = $("script").value;
218+
if (!script.trim()) { setStatus("nothing to compare", true); return; }
219+
try {
220+
setStatus("comparing modes…");
221+
const td = await api("/load?mode=topdown", script).then(() => api("/run"));
222+
const bu = await api("/load?mode=bottomup", script).then(() => api("/run"));
223+
const key = s => JSON.stringify([s.vars, [...s.rows].map(r => r.join("")).sort()]);
224+
const same = key(td) === key(bu);
225+
$("compare").innerHTML =
226+
`<h2>Mode comparison — final result sets ${same
227+
? '<span style="color:#2e7d32">(identical)</span>'
228+
: '<span style="color:#c62828">(DIVERGENT)</span>'}</h2>
229+
<div class="cmp">
230+
<div><b style="font-size:12px">topdown (correlated)</b>${rsTable(td, false)}</div>
231+
<div><b style="font-size:12px">bottomup (SPARQL algebra)</b>${rsTable(bu, false)}</div>
232+
</div>`;
233+
// restore the selected mode's fresh session
234+
prevRowCount = null;
235+
state = await api("/load?mode=" + $("mode").value, script);
236+
render();
237+
setStatus(same ? "modes agree on this pipeline"
238+
: "modes DIVERGE — inspect with step + try", !same);
239+
} catch (e) { setStatus("server error: " + e.message, true); }
240+
};
241+
242+
/* ── rendering ──────────────────────────────────────────────────────────── */
243+
244+
/** pretty-print an action-algebra s-expression with indentation */
245+
function prettyAlg(s) {
246+
let out = "", depth = 0;
247+
const BREAKERS = new Set(["djoin","join","optjoin","leftjoin","notexists","minus",
248+
"orelse","union","munion","diff","scope","pipeline",
249+
"ask","collect","ingraph"]);
250+
const toks = s.match(/\(|\)|[^()\s]+|\s+/g) ?? [];
251+
let lineStart = true;
252+
for (let i = 0; i < toks.length; ++i) {
253+
const t = toks[i];
254+
if (/^\s+$/.test(t)) { if (!lineStart) out += " "; continue; }
255+
if (t === "(") {
256+
const head = toks[i+1] ?? "";
257+
if (BREAKERS.has(head) && !lineStart) { out += "\n" + " ".repeat(depth); }
258+
out += "("; depth++; lineStart = false;
259+
} else if (t === ")") { depth--; out += ")"; lineStart = false; }
260+
else { out += t; lineStart = false; }
261+
}
262+
return out;
263+
}
264+
265+
function classify(alg) {
266+
if (alg.startsWith("(load")) return ["load", "dataset construction (merge / named graph)"];
267+
if (alg.startsWith("(ask")) return ["ask", "pattern matching over the current result set"];
268+
if (alg.startsWith("(collect")) {
269+
if (alg.includes("(groupby")) return ["collect", "GROUP BY + aggregate projection"];
270+
return ["collect", "projection / DISTINCT / ORDER / LIMIT"];
271+
}
272+
if (alg.startsWith("(filter")) return ["filter", "standalone filter (HAVING when after an aggregating collect)"];
273+
if (alg.startsWith("(bindings")) return ["bindings","inline table join (VALUES)"];
274+
if (alg.startsWith("(let")) return ["let", "computed binding (BIND)"];
275+
if (alg.startsWith("(test")) return ["test", "boolean probe (ASK) — result set unchanged"];
276+
if (alg.startsWith("(assert")) return ["assert", "template instantiation (CONSTRUCT/INSERT)"];
277+
if (alg.startsWith("(fwrule")) return ["fwrule", "forward rule to fixpoint"];
278+
return ["", ""];
279+
}
280+
281+
function rsTable(s, withProofs) {
282+
if (!s.vars.length && !s.rows.length) return "<em style='font-size:12px'>(unit result set)</em>";
283+
if (!s.rows.length) return "<em style='font-size:12px'>(empty result set)</em>";
284+
const hdr = s.vars.map(v => `<th>${esc(v)}</th>`).join("");
285+
const body = s.rows.map((r, i) => {
286+
const tds = r.map(c => `<td class="${c ? "" : "nil"}">${c ? esc(c) : "∅"}</td>`).join("");
287+
const proofs = withProofs && s.proofs[i] && s.proofs[i].length
288+
? `<tr class="proofrow"><td colspan="${s.vars.length}">⊢ ${s.proofs[i].map(esc).join(" · ")}</td></tr>`
289+
: "";
290+
return `<tr>${tds}</tr>${proofs}`;
291+
}).join("");
292+
return `<table class="sp"><thead><tr>${hdr}</tr></thead><tbody>${body}</tbody></table>`;
293+
}
294+
295+
function render() {
296+
if (!state) return;
297+
$("modeTag").textContent = "mode: " + state.mode;
298+
299+
$("pipeline").innerHTML = state.actions.map((a, i) => {
300+
const cls = i < state.pos ? "done" : i === state.pos && !state.done ? "now" : "";
301+
const [kind, gloss] = classify(a);
302+
return `<div class="act ${cls}" onclick="runTo(${i})" title="${esc(gloss)}">
303+
<span class="ord">${i}</span>
304+
<span class="alg">${esc(prettyAlg(a))}</span>
305+
</div>`;
306+
}).join("") || "<em style='font-size:12px;color:#999'>no actions</em>";
307+
308+
$("rowcount").textContent = "(" + state.rows.length + " row" + (state.rows.length === 1 ? "" : "s") + ")";
309+
if (prevRowCount !== null && prevRowCount !== state.rows.length) {
310+
const grow = state.rows.length > prevRowCount;
311+
$("delta").innerHTML = `rows: ${prevRowCount} → <b class="${grow ? "grow" : "shrink"}">${state.rows.length}</b>`;
312+
} else
313+
$("delta").innerHTML = "";
314+
$("rswrap").innerHTML = rsTable(state, $("showProofs").checked);
315+
316+
const extras = [];
317+
if (state.asserted.length)
318+
extras.push(`<details><summary>${state.asserted.length} asserted triple(s)</summary><pre>` +
319+
state.asserted.map(esc).join("\n") + "</pre></details>");
320+
if (state.actions.some(a => a.startsWith("(test")))
321+
extras.push(`test: <b>${state.lastTest ? "yes" : "no"}</b>`);
322+
$("extras").innerHTML = extras.join(" ");
323+
}
324+
</script>
325+
</body>
326+
</html>

‎bin/algae3.cpp‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131

3232
namespace sw = w3c_sw;
3333

34+
#include "algae3serve.inc"
35+
3436
namespace {
3537

3638
sw::AtomFactory F;
@@ -301,6 +303,8 @@ namespace {
301303

302304
int main (int argc, char** argv) {
303305
bool debug = false, algebra = false, proofs = false, sortedBindings = false;
306+
int servePort = 0;
307+
std::string serveDir = ".";
304308
int modeOverride = -1;
305309
std::vector<std::string> files;
306310

@@ -310,6 +314,8 @@ int main (int argc, char** argv) {
310314
else if (arg == "--algebra") algebra = true;
311315
else if (arg == "--proofs") proofs = true;
312316
else if (arg == "--sorted-bindings") sortedBindings = true; // test-golden format
317+
else if (arg == "--serve" && i+1 < argc) servePort = atoi(argv[++i]);
318+
else if (arg == "--serve-dir" && i+1 < argc) serveDir = argv[++i];
313319
else if (arg == "--mode" && i+1 < argc) {
314320
std::string m = argv[++i];
315321
if (m == "topdown") modeOverride = sw::a3::EVAL_topdown;
@@ -321,6 +327,8 @@ int main (int argc, char** argv) {
321327
} else
322328
files.push_back(arg);
323329
}
330+
if (servePort > 0)
331+
return a3serve::serve(servePort, &F, serveDir);
324332
if (files.empty()) {
325333
std::cerr << "usage: algae3 [--algebra] [--proofs] [--mode m] [--debug] file.a3 ...\n";
326334
return 2;

0 commit comments

Comments
 (0)