|
| 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,"&").replace(/</g,"<"); |
| 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(" |