@@ -54,11 +54,12 @@ async function buildModel(ctx, doc) {
5454 if ( s . available ) {
5555 // Prefer the --json stages (token names, AST source lines); older lpcc
5656 // rejects --json, so fall back to parsing the human text formats.
57- const [ pp , toksJ , astJ , bytecode ] = await Promise . all ( [
57+ const [ pp , toksJ , astJ , bytecode , bytecodeO0 ] = await Promise . all ( [
5858 lpcc . runStage ( s , s . relPath , 'preprocessed' ) ,
5959 lpcc . runStage ( s , s . relPath , 'tokensJson' ) ,
6060 lpcc . runStage ( s , s . relPath , 'astJson' ) ,
6161 lpcc . runStage ( s , s . relPath , 'bytecode' ) ,
62+ lpcc . runStage ( s , s . relPath , 'bytecodeO0' ) ,
6263 ] ) ;
6364 let tokens = lpcc . tokensFromJson ( lpcc . parseEnvelopes ( toksJ . raw ) ) ;
6465 if ( tokens === null ) {
@@ -81,6 +82,7 @@ async function buildModel(ctx, doc) {
8182 tokens,
8283 ast,
8384 bytecode : bytecode . ok ? lpcc . parseBytecode ( bytecode . raw ) : null ,
85+ bytecodeO0 : bytecodeO0 . ok ? lpcc . parseBytecode ( bytecodeO0 . raw ) : null ,
8486 diagnostics : bytecode . diagnostics ,
8587 } ;
8688 }
@@ -256,6 +258,19 @@ function webviewHtml(webview) {
256258 svg .gnode text { fill: var(--vscode-editor-foreground); font-size: 11px; }
257259 svg path.edge { fill: none; stroke: var(--vscode-panel-border, #888a); }
258260 /* bytecode */
261+ #bc-bar { display: flex; gap: 14px; align-items: center; margin: 4px 0 8px; }
262+ #bc-bar label { cursor: pointer; }
263+ tr.ins.haslink { cursor: pointer; }
264+ #bc-tip { position: fixed; z-index: 10; display: none; max-width: 640px; overflow: hidden;
265+ background: var(--vscode-editorWidget-background, #252526);
266+ border: 1px solid var(--vscode-focusBorder, #07f); border-radius: 4px;
267+ padding: 6px 10px; box-shadow: 0 4px 12px #0008; pointer-events: none; }
268+ #bc-tip .tip-file { opacity: .6; font-size: 11px; margin-bottom: 3px; }
269+ #bc-tip .srcline { white-space: pre; }
270+ #bc-tip .srcline .ln { display: inline-block; width: 2.6em; text-align: right; padding-right: 8px;
271+ opacity: .45; }
272+ #bc-tip .srcline.cur { background: var(--vscode-editor-selectionHighlightBackground, #07f3);
273+ border-radius: 2px; }
259274 details { margin: 6px 0; }
260275 summary { cursor: pointer; font-weight: 600; }
261276 summary .jump { font-weight: 400; margin-left: 8px; }
@@ -411,9 +426,10 @@ function webviewHtml(webview) {
411426 model.lpcc.ast.forEach((sec, si) => {
412427 html += '<h3>' + esc(sec.title) + '</h3><ul class="ast root">';
413428 sec.roots.forEach((r, ri) => {
414- // TREE_MAIN roots are (function ...) in source definition order.
429+ // TREE_MAIN roots are (function ...) in source definition order
430+ // (JSON labels carry the function index: "function 2").
415431 let label = r.label;
416- if (label === ' function' && si === 0 && fnNames[ri]) label += ' — ' + fnNames[ri] + '()';
432+ if (/^ function($| )/.test(label) && si === 0 && fnNames[ri]) label += ' — ' + fnNames[ri] + '()';
417433 html += renderAstNode({ ...r, label }, [si, ri], ctxInfo);
418434 });
419435 html += '</ul>';
@@ -464,8 +480,15 @@ function webviewHtml(webview) {
464480 }
465481
466482 function drawAstGraph(el, ctxInfo) {
467- const root = astSelPath.length >= 2 ? findAstNode(astSelPath)
468- : (model.lpcc.ast[0] && model.lpcc.ast[0].roots[0]) || null;
483+ // Graph the selected subtree; a selected LEAF graphs its parent so the
484+ // pane always shows context, not a single lonely box.
485+ let selPath = astSelPath;
486+ let root = selPath.length >= 2 ? findAstNode(selPath) : null;
487+ while (root && root.children.length === 0 && selPath.length > 2) {
488+ selPath = selPath.slice(0, -1);
489+ root = findAstNode(selPath);
490+ }
491+ if (!root) root = (model.lpcc.ast[0] && model.lpcc.ast[0].roots[0]) || null;
469492 if (!root) { el.innerHTML = '<p class="muted">Select a node to graph its subtree.</p>'; return; }
470493 // Tidy-ish layout: x from leaf ordering, y from depth. Cap size.
471494 const MAXN = 400;
@@ -509,19 +532,42 @@ function webviewHtml(webview) {
509532 if (lbl.length > 24) lbl = lbl.slice(0, 23) + '…';
510533 svg += '<g class="gnode' + (i === 0 && astSelPath.length >= 2 ? ' sel' : '') + '">' +
511534 '<rect x="' + (m.x + PAD) + '" y="' + (m.depth * H + PAD) + '" width="' + m.w + '" height="22"/>' +
512- '<text x="' + (m.x + m.w / 2 + PAD) + '" y="' + (m.depth * H + 25 + PAD) + '" text-anchor="middle">' + esc(lbl) + '</text></g>';
535+ '<text x="' + (m.x + m.w / 2 + PAD) + '" y="' + (m.depth * H + 15 + PAD) + '" text-anchor="middle">' + esc(lbl) + '</text></g>';
513536 });
514537 svg += '</svg>';
515538 el.innerHTML = (count >= MAXN ? '<p class="muted">Subtree truncated at ' + MAXN + ' nodes — select a smaller node.</p>' : '') + svg;
516539 }
517540
518541 // ---- Bytecode ------------------------------------------------------------
542+ let bcMode = 'opt'; // 'opt' (default optimized dump) | 'O0'
543+
544+ function srcLineHtml() {
545+ // Line-addressable syntax-colored source: tokens re-chunked per line.
546+ // NB: this code lives inside webviewHtml's outer template literal --
547+ // escape sequences must be doubled or they expand at THAT level.
548+ const lines = [[]];
549+ for (const t of model.tokens) {
550+ const parts = t.text.split('\\n');
551+ parts.forEach((p, i) => {
552+ if (i > 0) lines.push([]);
553+ if (p) lines[lines.length - 1].push('<span class="lpc-' + t.kind + '">' + esc(p) + '</span>');
554+ });
555+ }
556+ return lines.map((spans) => spans.join(''));
557+ }
558+
519559 function renderBytecode(el) {
520560 if (!model.lpcc.available) { el.innerHTML = lpccHint('the bytecode view'); return; }
521- const bc = model.lpcc.bytecode;
561+ const bc = bcMode === 'O0' ? (model.lpcc.bytecodeO0 || model.lpcc.bytecode) : model.lpcc.bytecode;
522562 if (!bc) { el.innerHTML = '<div class="hint">No bytecode captured — the file may not compile; see Problems.</div>'; return; }
523563 const outlineByName = new Map((model.outline.functions || []).map((f) => [f.name, f]));
524- let html = '<h3>' + esc(bc.name || model.file) + '</h3>';
564+ let html = '<div id="bc-bar"><strong>' + esc(bc.name || model.file) + '</strong>' +
565+ '<label><input type="radio" name="bcmode" value="opt"' + (bcMode === 'opt' ? ' checked' : '') +
566+ '> optimized</label>' +
567+ '<label><input type="radio" name="bcmode" value="O0"' + (bcMode === 'O0' ? ' checked' : '') +
568+ '> -O0 (optimizer off)</label>' +
569+ (bcMode === 'O0' && !model.lpcc.bytecodeO0 ? '<span class="muted">-O0 dump unavailable; showing optimized</span>' : '') +
570+ '<span class="muted">hover a row for its source; click to jump</span></div>';
525571 html += '<details><summary>Program tables</summary>' +
526572 '<h4>Functions</h4><table>' + bc.functionsTable.map((f) =>
527573 '<tr><td class="muted">' + f.index + '</td><td>' + esc(f.name) + '</td></tr>').join('') + '</table>' +
@@ -536,12 +582,16 @@ function webviewHtml(webview) {
536582 (o ? ' <a class="link jump" data-l="' + o.line + '" data-c="' + o.col + '">go to source ↗</a>' : '') +
537583 '</summary><table><tr><th>addr</th><th>bytes</th><th>instruction</th><th>operands</th><th>src</th></tr>';
538584 for (const ins of fn.instructions) {
539- let comment = esc(ins.comment);
585+ // Garbage rows from the known disassembler decode bug can be huge.
586+ let comment = esc(ins.comment.length > 120 ? ins.comment.slice(0, 117) + '…' : ins.comment);
540587 if (ins.target) {
541588 comment = comment.replace('(' + ins.target + ')',
542589 '(<a class="link tgt" data-a="' + ins.target + '">' + ins.target + '</a>)');
543590 }
544- html += '<tr id="a' + ins.addr + '"><td class="addr">' + ins.addr + '</td><td class="hex">' +
591+ const inFile = ins.srcFile && model.lpcc.relPath &&
592+ (ins.srcFile === model.lpcc.relPath || '/' + ins.srcFile === model.lpcc.relPath);
593+ html += '<tr id="a' + ins.addr + '" class="ins' + (inFile ? ' haslink' : '') +
594+ (inFile ? '" data-srcl="' + ins.srcLine : '') + '"><td class="addr">' + ins.addr + '</td><td class="hex">' +
545595 esc(ins.hex.length > 26 ? ins.hex.slice(0, 24) + '…' : ins.hex) + '</td><td>' + esc(ins.mnemonic) +
546596 '</td><td>' + comment + '</td><td>' +
547597 (ins.srcLine ? '<a class="link src" data-f="' + esc(ins.srcFile) + '" data-l="' + ins.srcLine +
@@ -550,7 +600,35 @@ function webviewHtml(webview) {
550600 }
551601 html += '</table></details>';
552602 }
603+ html += '<div id="bc-tip"></div>';
553604 el.innerHTML = html;
605+ // mode toggle
606+ el.querySelectorAll('input[name=bcmode]').forEach((r) => r.onchange = () => {
607+ bcMode = r.value; render();
608+ });
609+ // hover box: the source line (with a little context) for this instruction
610+ const lines = srcLineHtml();
611+ const tip = el.querySelector('#bc-tip');
612+ el.querySelectorAll('tr.ins.haslink').forEach((tr) => {
613+ const l = +tr.dataset.srcl;
614+ tr.onmouseenter = () => {
615+ let body = '';
616+ for (let i = Math.max(1, l - 1); i <= Math.min(lines.length, l + 1); i++) {
617+ body += '<div class="srcline' + (i === l ? ' cur' : '') + '"><span class="ln">' + i +
618+ '</span>' + (lines[i - 1] || '') + '</div>';
619+ }
620+ tip.innerHTML = '<div class="tip-file">' + esc(model.file) + ':' + l + '</div>' + body;
621+ tip.style.display = 'block';
622+ };
623+ tr.onmousemove = (ev) => {
624+ const pad = 14;
625+ tip.style.left = Math.min(ev.clientX + pad, window.innerWidth - tip.offsetWidth - 8) + 'px';
626+ tip.style.top = (ev.clientY + pad + tip.offsetHeight > window.innerHeight
627+ ? ev.clientY - tip.offsetHeight - pad : ev.clientY + pad) + 'px';
628+ };
629+ tr.onmouseleave = () => { tip.style.display = 'none'; };
630+ tr.onclick = () => post({ type: 'reveal', line: l, col: 1 });
631+ });
554632 el.querySelectorAll('a.jump').forEach((a) => a.onclick = (e) => {
555633 e.stopPropagation(); e.preventDefault();
556634 post({ type: 'reveal', line: +a.dataset.l, col: +a.dataset.c });
0 commit comments