lisp-machine / soulvm-debugger.html
SNAPKITTYWEST's picture
Deploy interactive lisp-machine Space
4cd921e verified
Raw
History Blame Contribute Delete
20.5 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SoulVM Source-Level Debugger</title>
<style>
:root {
--bg-void: #020508;
--bg-deep: #050b12;
--line-medium: rgba(112, 220, 255, 0.28);
--line-bright: rgba(112, 220, 255, 0.72);
--text-primary: #e8fbff;
--text-secondary: #8eb5c1;
--text-muted: #52707a;
--cyan: #56e8ff;
--cyan-hot: #a9f7ff;
--teal: #38ffc7;
--violet: #ab7cff;
--gold: #ffc857;
--red: #ff5470;
--green: #5dff9d;
--shadow-cyan: 0 0 18px rgba(86, 232, 255, 0.2), 0 0 52px rgba(86, 232, 255, 0.08);
--radius-sm: 10px;
--radius-md: 16px;
--radius-lg: 24px;
--font-display: "Orbitron", "Rajdhani", "Segoe UI", sans-serif;
--font-mono: "JetBrains Mono", "IBM Plex Mono", "Cascadia Code", monospace;
--ease-system: cubic-bezier(0.22, 1, 0.36, 1);
}
* { box-sizing: border-box; }
html { min-height: 100%; background: var(--bg-void); }
body {
min-height: 100vh;
margin: 0;
color: var(--text-primary);
font-family: var(--font-mono);
background: linear-gradient(180deg, #020508 0%, #041019 48%, #020609 100%);
}
button, input, textarea { font: inherit; color: inherit; }
.debugger-shell {
width: min(1800px, 100%);
margin: 0 auto;
padding: 18px;
}
.debugger-header {
position: sticky;
top: 12px;
z-index: 30;
display: flex;
gap: 16px;
align-items: center;
min-height: 76px;
padding: 14px 18px;
border: 1px solid var(--line-medium);
border-radius: var(--radius-lg);
background: linear-gradient(135deg, rgba(9, 24, 36, 0.94), rgba(4, 12, 19, 0.91));
box-shadow: var(--shadow-cyan), inset 0 1px rgba(255, 255, 255, 0.04);
backdrop-filter: blur(22px) saturate(135%);
margin-bottom: 20px;
}
.debugger-title {
font-family: var(--font-display);
font-size: 1.35rem;
font-weight: 700;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(--teal);
}
.debugger-controls {
display: flex;
gap: 8px;
margin-left: auto;
}
.control-btn {
padding: 8px 14px;
border: 1px solid var(--line-medium);
background: linear-gradient(180deg, rgba(18, 47, 65, 0.9), rgba(6, 19, 28, 0.95));
color: var(--text-primary);
border-radius: var(--radius-sm);
cursor: pointer;
transition: all 160ms var(--ease-system);
font-size: 0.75rem;
font-weight: bold;
text-transform: uppercase;
}
.control-btn:hover {
border-color: var(--line-bright);
box-shadow: var(--shadow-cyan);
}
.control-btn[data-state="running"] {
background: linear-gradient(135deg, var(--cyan-hot), var(--cyan));
color: #001115;
border-color: transparent;
}
.debugger-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 20px;
}
.panel {
border: 1px solid var(--line-medium);
border-radius: var(--radius-md);
background: linear-gradient(160deg, rgba(10, 26, 38, 0.9), rgba(3, 11, 17, 0.96));
box-shadow: 0 20px 80px rgba(0, 0, 0, 0.26), inset 0 1px rgba(255, 255, 255, 0.035);
overflow: hidden;
}
.panel-header {
display: flex;
gap: 12px;
align-items: center;
min-height: 52px;
padding: 0 16px;
border-bottom: 1px solid var(--line-medium);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.025), transparent);
}
.panel-title {
color: var(--cyan-hot);
font-family: var(--font-display);
font-size: 0.78rem;
letter-spacing: 0.14em;
text-transform: uppercase;
margin: 0;
}
.panel-body {
padding: 14px;
}
textarea {
width: 100%;
height: 200px;
padding: 12px;
border: 1px solid rgba(86, 232, 255, 0.1);
background: #03080d;
color: var(--cyan);
border-radius: var(--radius-sm);
font-family: var(--font-mono);
font-size: 0.9rem;
resize: vertical;
outline: 0;
}
textarea:focus {
border-color: var(--cyan);
box-shadow: 0 0 10px rgba(86, 232, 255, 0.1);
}
.bytecode-list {
display: flex;
flex-direction: column;
gap: 4px;
max-height: 300px;
overflow-y: auto;
font-size: 0.8rem;
}
.bytecode-line {
padding: 6px 8px;
border: 1px solid transparent;
border-left: 2px solid rgba(86, 232, 255, 0.2);
background: rgba(2, 10, 16, 0.4);
cursor: pointer;
transition: all 100ms var(--ease-system);
}
.bytecode-line:hover {
border-left-color: var(--cyan);
background: rgba(86, 232, 255, 0.05);
}
.bytecode-line[data-active="true"] {
border-left: 3px solid var(--teal);
background: rgba(56, 255, 199, 0.1);
color: var(--teal-hot, #5dff9d);
}
.ip-marker {
display: inline-block;
width: 16px;
height: 16px;
margin-right: 8px;
border-radius: 50%;
background: var(--teal);
font-size: 0.6rem;
text-align: center;
line-height: 16px;
color: #020609;
font-weight: bold;
}
.vm-state-panel {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
.vm-register {
padding: 12px;
border: 1px solid var(--line-medium);
background: rgba(2, 10, 16, 0.55);
border-radius: var(--radius-sm);
}
.vm-reg-label {
color: var(--text-muted);
font-size: 0.65rem;
text-transform: uppercase;
letter-spacing: 0.1em;
margin-bottom: 6px;
}
.vm-reg-value {
color: var(--teal);
font-size: 1.1rem;
font-weight: bold;
font-family: var(--font-mono);
}
.execution-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.execution-grid.full {
grid-column: 1 / -1;
}
.stack-view {
max-height: 250px;
overflow-y: auto;
display: flex;
flex-direction: column-reverse;
gap: 4px;
}
.stack-item {
padding: 6px 8px;
background: rgba(56, 255, 199, 0.1);
border-left: 2px solid var(--teal);
border-radius: 3px;
font-size: 0.8rem;
color: var(--teal);
animation: stack-pulse 0.5s ease-out;
}
@keyframes stack-pulse {
0% { background: rgba(56, 255, 199, 0.3); transform: scale(1.05); }
100% { background: rgba(56, 255, 199, 0.1); transform: scale(1); }
}
.trace-entry {
padding: 6px 8px;
border-bottom: 1px solid rgba(112, 220, 255, 0.1);
font-size: 0.75rem;
color: var(--text-secondary);
}
.trace-entry:last-child {
border-bottom: 0;
}
.trace-entry.current {
background: rgba(112, 220, 255, 0.05);
border-left: 2px solid var(--cyan);
padding-left: 6px;
color: var(--cyan);
}
.source-highlight {
padding: 8px;
background: rgba(171, 124, 255, 0.1);
border-left: 2px solid var(--violet);
border-radius: 3px;
font-size: 0.8rem;
color: var(--violet);
margin-top: 12px;
}
.section-title {
color: var(--text-muted);
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.1em;
margin: 12px 0 8px;
padding-top: 12px;
border-top: 1px solid rgba(112, 220, 255, 0.1);
}
@media (max-width: 1024px) {
.debugger-grid {
grid-template-columns: 1fr;
}
.vm-state-panel {
grid-template-columns: 1fr;
}
.execution-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="debugger-shell">
<div class="debugger-header">
<h1 class="debugger-title">🔬 SoulVM Debugger</h1>
<span style="color: var(--text-muted); font-size: 0.8rem;">Source-level execution tracer</span>
<div class="debugger-controls">
<button class="control-btn" onclick="compileAndStep()" title="Compile and step">⏭️ Step</button>
<button class="control-btn" onclick="runFull()" data-state="running" title="Run to completion">▶️ Run</button>
<button class="control-btn" onclick="reset()" title="Reset">↻ Reset</button>
</div>
</div>
<div class="debugger-grid">
<!-- SOURCE CODE -->
<div class="panel">
<div class="panel-header">
<h2 class="panel-title">📝 LISP Source</h2>
</div>
<div class="panel-body">
<textarea id="source-input" spellcheck="false">(+ 1 2)</textarea>
<button onclick="compile()" style="width: 100%; margin-top: 12px; padding: 10px; border: 1px solid var(--line-medium); background: linear-gradient(135deg, var(--cyan-hot), var(--cyan)); color: #001115; border-radius: var(--radius-sm); cursor: pointer; font-weight: bold; text-transform: uppercase;">⚙️ Compile</button>
</div>
</div>
<!-- BYTECODE -->
<div class="panel">
<div class="panel-header">
<h2 class="panel-title">📦 Bytecode</h2>
<span style="margin-left: auto; color: var(--text-muted); font-size: 0.7rem;" id="bytecode-count">0 ops</span>
</div>
<div class="panel-body">
<div class="bytecode-list" id="bytecode-list">
<div style="color: var(--text-muted); font-size: 0.75rem;">Compile source to view bytecode...</div>
</div>
</div>
</div>
<!-- VM STATE -->
<div class="panel">
<div class="panel-header">
<h2 class="panel-title">⚙️ VM State</h2>
</div>
<div class="panel-body">
<div class="vm-state-panel">
<div class="vm-register">
<div class="vm-reg-label">IP (Instr Ptr)</div>
<div class="vm-reg-value" id="vm-ip">0</div>
</div>
<div class="vm-register">
<div class="vm-reg-label">SP (Stack Ptr)</div>
<div class="vm-reg-value" id="vm-sp">0</div>
</div>
<div class="vm-register">
<div class="vm-reg-label">Instr Count</div>
<div class="vm-reg-value" id="vm-instrs">0</div>
</div>
</div>
<div class="section-title">Current Opcode</div>
<div style="padding: 8px; background: rgba(56, 255, 199, 0.1); border-radius: 3px; color: var(--teal); font-weight: bold;" id="current-opcode"></div>
<div class="section-title">Source Expression</div>
<div class="source-highlight" id="source-highlight">Awaiting execution...</div>
</div>
</div>
<!-- STACK VIEW -->
<div class="panel">
<div class="panel-header">
<h2 class="panel-title">📚 Stack</h2>
<span style="margin-left: auto; color: var(--text-muted); font-size: 0.7rem;" id="stack-depth">depth: 0</span>
</div>
<div class="panel-body">
<div class="stack-view" id="stack-view">
<div style="color: var(--text-muted); font-size: 0.75rem;">Empty</div>
</div>
</div>
</div>
<!-- EXECUTION TRACE -->
<div class="panel">
<div class="panel-header">
<h2 class="panel-title">📋 Trace</h2>
<span style="margin-left: auto; color: var(--text-muted); font-size: 0.7rem;" id="trace-count">0 steps</span>
</div>
<div class="panel-body">
<div style="max-height: 300px; overflow-y: auto;">
<div id="trace-list">
<div class="trace-entry" style="color: var(--text-muted);">Ready to execute...</div>
</div>
</div>
</div>
</div>
<!-- FINAL RESULT -->
<div class="panel execution-grid full">
<div class="panel-header">
<h2 class="panel-title">✨ Result</h2>
</div>
<div class="panel-body">
<div style="padding: 16px; background: rgba(93, 255, 157, 0.1); border: 1px solid rgba(93, 255, 157, 0.3); border-radius: var(--radius-sm);">
<div style="color: var(--text-muted); font-size: 0.75rem; margin-bottom: 6px;">Output</div>
<div style="color: var(--green); font-size: 1.2rem; font-weight: bold; font-family: var(--font-mono);" id="result-output"></div>
</div>
</div>
</div>
</div>
</div>
<script>
// ========================================================================
// State
// ========================================================================
const state = {
source: '',
bytecode: [],
stack: [],
ip: 0,
trace: [],
result: null,
sourceMap: {}, // Maps IP to source expression
};
// ========================================================================
// LISP Compiler
// ========================================================================
function tokenize(source) {
const tokens = [];
let i = 0;
while (i < source.length) {
const ch = source[i];
if (/\s/.test(ch)) {
i++;
} else if (ch === '(' || ch === ')') {
tokens.push(ch);
i++;
} else if (/[-\d]/.test(ch)) {
let num = '';
while (i < source.length && /[-\d]/.test(source[i])) {
num += source[i];
i++;
}
tokens.push(parseInt(num));
} else {
let sym = '';
while (i < source.length && !/[\s()]/. test(source[i])) {
sym += source[i];
i++;
}
tokens.push(sym);
}
}
return tokens;
}
function parse(tokens, pos = 0) {
if (pos >= tokens.length) return { error: 'Unexpected end' };
const token = tokens[pos];
if (token === '(') {
const list = [];
pos++;
while (pos < tokens.length && tokens[pos] !== ')') {
const result = parse(tokens, pos);
if (result.error) return result;
list.push(result.ast);
pos = result.pos;
}
if (pos >= tokens.length) return { error: 'Unmatched paren' };
return { ast: { type: 'list', elements: list }, pos: pos + 1 };
} else if (typeof token === 'number') {
return { ast: { type: 'number', value: token }, pos: pos + 1 };
} else {
return { ast: { type: 'symbol', value: token }, pos: pos + 1 };
}
}
function compileToByteCode(ast, sourceExpr = '') {
const bytecode = [];
const sourceMap = {};
function compile(node) {
if (node.type === 'number') {
const ip = bytecode.length;
bytecode.push({ op: 'PUSH', operand: node.value });
sourceMap[ip] = `push ${node.value}`;
return;
}
if (node.type === 'symbol') {
const ip = bytecode.length;
bytecode.push({ op: 'PUSH', operand: node.value });
sourceMap[ip] = `${node.value}`;
return;
}
if (node.type === 'list' && node.elements.length > 0) {
const [fn, ...args] = node.elements;
// Compile args first
args.forEach(arg => compile(arg));
// Then the operation
if (fn.type === 'symbol') {
const op = fn.value === '+' ? 'ADD' :
fn.value === '-' ? 'SUB' :
fn.value === '*' ? 'MUL' : 'UNKNOWN';
const ip = bytecode.length;
bytecode.push({ op });
sourceMap[ip] = `(${fn.value} ...)`;
}
}
}
compile(ast);
bytecode.push({ op: 'RET' });
return { bytecode, sourceMap };
}
// ========================================================================
// SoulVM Executor
// ========================================================================
function executeStep() {
if (state.ip >= state.bytecode.length) return false;
const instr = state.bytecode[state.ip];
state.trace.push({
ip: state.ip,
op: instr.op,
operand: instr.operand,
stackBefore: [...state.stack],
});
switch (instr.op) {
case 'PUSH':
state.stack.push(instr.operand);
break;
case 'ADD':
state.stack.push(state.stack.pop() + state.stack.pop());
break;
case 'SUB':
const b = state.stack.pop();
state.stack.push(state.stack.pop() - b);
break;
case 'MUL':
state.stack.push(state.stack.pop() * state.stack.pop());
break;
case 'RET':
state.result = state.stack.length > 0 ? state.stack[state.stack.length - 1] : null;
return false;
default:
return false;
}
state.ip++;
return state.ip < state.bytecode.length;
}
// ========================================================================
// UI Functions
// ========================================================================
function compile() {
const source = document.getElementById('source-input').value.trim();
if (!source) return;
state.source = source;
state.stack = [];
state.ip = 0;
state.trace = [];
state.result = null;
const tokens = tokenize(source);
const parseResult = parse(tokens);
if (parseResult.error) {
alert('Parse error: ' + parseResult.error);
return;
}
const compiled = compileToByteCode(parseResult.ast, source);
state.bytecode = compiled.bytecode;
state.sourceMap = compiled.sourceMap;
renderBytecode();
renderVMState();
renderStack();
renderTrace();
}
function compileAndStep() {
if (state.bytecode.length === 0) {
compile();
}
executeStep();
renderVMState();
renderStack();
renderTrace();
}
function runFull() {
if (state.bytecode.length === 0) {
compile();
}
while (executeStep()) {}
renderVMState();
renderStack();
renderTrace();
renderResult();
}
function reset() {
state.stack = [];
state.ip = 0;
state.trace = [];
state.result = null;
renderVMState();
renderStack();
renderTrace();
document.getElementById('result-output').textContent = '—';
}
// ========================================================================
// Rendering
// ========================================================================
function renderBytecode() {
const list = document.getElementById('bytecode-list');
document.getElementById('bytecode-count').textContent = `${state.bytecode.length} ops`;
list.innerHTML = state.bytecode
.map((instr, idx) => `
<div class="bytecode-line" data-active="${idx === state.ip ? 'true' : 'false'}" onclick="jumpToIP(${idx})">
${idx === state.ip ? '<div class="ip-marker">▶</div>' : '<span style="display:inline-block;width:24px;"></span>'}
<strong>${idx}:</strong> ${instr.op}${instr.operand !== undefined ? ' ' + instr.operand : ''}
</div>
`)
.join('');
}
function renderVMState() {
document.getElementById('vm-ip').textContent = state.ip;
document.getElementById('vm-sp').textContent = state.stack.length;
document.getElementById('vm-instrs').textContent = state.bytecode.length;
const instr = state.bytecode[state.ip];
if (instr) {
document.getElementById('current-opcode').textContent = `${instr.op}${instr.operand !== undefined ? ' ' + instr.operand : ''}`;
document.getElementById('source-highlight').textContent = state.sourceMap[state.ip] || '(source mapping)';
}
renderBytecode();
}
function renderStack() {
const view = document.getElementById('stack-view');
document.getElementById('stack-depth').textContent = `depth: ${state.stack.length}`;
if (state.stack.length === 0) {
view.innerHTML = '<div style="color: var(--text-muted); font-size: 0.75rem;">Empty</div>';
return;
}
view.innerHTML = state.stack
.map((val, idx) => `<div class="stack-item">[${idx}] ${val}</div>`)
.join('');
}
function renderTrace() {
const list = document.getElementById('trace-list');
document.getElementById('trace-count').textContent = `${state.trace.length} steps`;
if (state.trace.length === 0) {
list.innerHTML = '<div class="trace-entry" style="color: var(--text-muted);">No execution yet...</div>';
return;
}
list.innerHTML = state.trace
.map((entry, idx) => `
<div class="trace-entry ${idx === state.trace.length - 1 ? 'current' : ''}">
<strong>Step ${idx + 1}:</strong> IP=${entry.ip}, OP=${entry.op} ${entry.operand !== undefined ? entry.operand : ''}<br>
<span style="font-size: 0.7rem;">Stack: [${entry.stackBefore.join(', ')}]</span>
</div>
`)
.join('');
}
function renderResult() {
if (state.result !== null) {
document.getElementById('result-output').textContent = state.result;
}
}
function jumpToIP(ip) {
state.ip = ip;
state.stack = state.trace[ip] ? [...state.trace[ip].stackBefore] : [];
renderVMState();
renderStack();
}
window.compile = compile;
window.compileAndStep = compileAndStep;
window.runFull = runFull;
window.reset = reset;
window.jumpToIP = jumpToIP;
</script>
</body>
</html>