| <!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">
|
|
|
| <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>
|
|
|
|
|
| <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>
|
|
|
|
|
| <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>
|
|
|
|
|
| <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>
|
|
|
|
|
| <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>
|
|
|
|
|
| <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>
|
|
|
|
|
|
|
|
|
| const state = {
|
| source: '',
|
| bytecode: [],
|
| stack: [],
|
| ip: 0,
|
| trace: [],
|
| result: null,
|
| sourceMap: {},
|
| };
|
|
|
|
|
|
|
|
|
|
|
| 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;
|
|
|
|
|
| args.forEach(arg => compile(arg));
|
|
|
|
|
| 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 };
|
| }
|
|
|
|
|
|
|
|
|
|
|
| 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;
|
| }
|
|
|
|
|
|
|
|
|
|
|
| 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 = '—';
|
| }
|
|
|
|
|
|
|
|
|
|
|
| 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>
|
|
|