Skip to content

Commit 0c427de

Browse files
thefallentreeclaude
andcommitted
Explorer polish: bytecode hover-source + -O0 toggle, AST fixes
Bytecode view: instruction rows get a floating hover box showing the syntax-colored source line (+/-1 context) that generated them, row click jumps the editor there, and a toolbar toggles the optimized dump vs the -O0 (tree-optimizer-off) dump -- buildModel now runs both stages. AST view: function roots regain their outline names under --json labels ('function 2 -- create()'), graphing a selected LEAF now graphs its parent subtree for context, and SVG node text is vertically centered. Bug class fixed twice now and worth remembering: the webview script lives inside webviewHtml's outer template literal, so escapes in it expand one level early -- '\n' in split() became a real newline and broke the whole script ('\b' in a regex did the same earlier). Escapes in webview code must be doubled. Fixtures re-captured from the fixed lpcc (upstream loop_cond_number decode desync): all four functions now disassemble, pinned by new tests alongside a -O0 fixture test. Verified by rendering every tab in Chromium and inspecting screenshots (hover box, toggle, AST graph). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AXqX4Kf55ArA39suS9cdCv
1 parent 98e8289 commit 0c427de

4 files changed

Lines changed: 240 additions & 19 deletions

File tree

extension/explorer.js

Lines changed: 88 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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 });

scripts/fixtures/sample.dis.txt

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ Loading simul_efun file : /single/simul_efun
2222
| ^
2323
note: while loading '/std/number_string' inherited by '/single/simul_efun'
2424
Loading master file: /single/master
25-
*Warning: unable to open stat file domain_stats for reading.
26-
*Warning: unable to open stat file author_stats for reading.
2725
NAME: /clone/explorer_sample.lpc
2826
INHERITS:
2927
name fio vio
@@ -78,14 +76,31 @@ DISASSEMBLY:
7876
0026: 45 00 00 global_lvalue ; counter(0)
7977
0029: 69 (void)+= ;
8078
002a: 23 02 loop_incr ; LV2
81-
002c: 22 02 03 00 00 00 00 00 loop_cond_number ; LV2 < 3 bbranch_when_non_zero 0000 (0032)
82-
0034: *** zero opcode ***
83-
0035: *** zero opcode ***
84-
0036: 18 00 02 branch_when_non_zero ; 0200 (0237)
79+
002c: 22 02 03 00 00 00 00 00 00 00 18 00 loop_cond_number ; LV2 < 3 branch back 0018 (001e)
8580
; clone/explorer_sample.lpc:10
86-
0039: 02 80 45 15 05 00 40 01 2F 0E 05 2F 30 0E 06 2D 01 00 01 01 30 0E 03 45 01 00 6A 30 00 00 00 78 A3 A4 73 CA 7F 00 00 03 00 00 00 00 00 00 00 46 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 05 00 00 00 38 A9 A4 73 CA 7F 00 00 05 00 01 01 02 00 00 00 06 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 F_PUSH ; CA 7F 00 00 05 00 01 01 02 00 00 00 06 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
81+
0038: 02 02 80 45 F_PUSH ; push global 0, number 5
82+
003c: 15 05 00 branch_le ; 0005 (0042)
83+
003f: 40 01 local ; LV1
84+
0041: 2F return ;
85+
0042: 0E 05 short_string ; ""
86+
0044: 2F return ;
87+
0045: 30 return_zero ;
8788
; clone/explorer_sample.lpc:11
8889

90+
;; Function: public void create()
91+
0046: 0E 06 short_string ; "world"
92+
0048: 2D 01 00 01 F_CALL_FUNCTION_BY_ADDRESS ; greet, pushed_args:1
93+
004c: 01 pop ;
94+
004d: 30 return_zero ;
95+
; clone/explorer_sample.lpc:16
96+
97+
;; Function: hidden void #global_init#()
98+
004e: 0E 03 short_string ; "sample"
99+
0050: 45 01 00 global_lvalue ; name(1)
100+
0053: 6A (void)assign ;
101+
0054: 30 return_zero ;
102+
; clone/explorer_sample.lpc:4
103+
89104
;;; *** Line Number Info ***
90105

91106
absolute line -> (file, line) table:
@@ -102,5 +117,5 @@ address -> absolute line table:
102117
0038-0045: 79
103118
0046-004d: 84
104119
004e-0054: 72
105-
Trace duration: 8350.347000 us, dumping 109 events to trace_lpcc.json in separate thread.
106-
[thread 140507348989632d]: Dump trace successfully to file trace_lpcc.json, cost 0 ms.
120+
Trace duration: 8778.028000 us, dumping 110 events to trace_lpcc.json in separate thread.
121+
[thread 139687047984832d]: Dump trace successfully to file trace_lpcc.json, cost 0 ms.

scripts/fixtures/sample.disO0.txt

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
Processing config file: etc/config.test
2+
maximum local variables: invalid new value, resetting to default.
3+
New Debug log location: "log/debug.log".
4+
Execution root: ./
5+
Initializing internal stuff ....
6+
Event backend in use: epoll
7+
Loading simul_efun file : /single/simul_efun
8+
/std/base64.lpc:53:1: warning: Unused local variable 'p'
9+
53 | }
10+
| ^
11+
note: while loading '/std/base64' inherited by '/single/simul_efun'
12+
/std/base64.lpc:53:1: warning: Unused local variable 'rlen'
13+
53 | }
14+
| ^
15+
note: while loading '/std/base64' inherited by '/single/simul_efun'
16+
/std/number_string.lpc:45:1: warning: Unused local variable 'parts'
17+
45 | }
18+
| ^
19+
note: while loading '/std/number_string' inherited by '/single/simul_efun'
20+
/std/number_string.lpc:45:1: warning: Unused local variable 'part'
21+
45 | }
22+
| ^
23+
note: while loading '/std/number_string' inherited by '/single/simul_efun'
24+
Loading master file: /single/master
25+
NAME: /clone/explorer_sample.lpc
26+
INHERITS:
27+
name fio vio
28+
---------------- --- ---
29+
FUNCTIONS:
30+
name offset mods flags fio vio # locals # args # def args
31+
------------------------- ------ ---- ------- --- --- -------- ------ ----------
32+
0: create 0 +-- --s---- 0 0 0
33+
1: greet 1 +-- --s---v 2 1 0
34+
2: add 2 +-- --s---- 0 2 0
35+
3: #global_init# 3 --- --s---- 0 0 0
36+
37+
;;; clone/explorer_sample.lpc
38+
39+
Globals:
40+
0: counter
41+
1: name
42+
VARIABLES defined:
43+
0: public int counter
44+
1: private string name
45+
STRINGS:
46+
0: clone/explorer_sample.lpc
47+
1: include/globals.h
48+
2: include/tests.h
49+
3: sample
50+
4: hello
51+
5:
52+
6: world
53+
DISASSEMBLY:
54+
55+
;; Function: public int add(int,int)
56+
0000: 02 02 C0 C1 F_PUSH ; push local 0, local 1
57+
0004: 6C + ;
58+
0005: 2F return ;
59+
; clone/explorer_sample.lpc:6
60+
61+
;; Function: public varargs string greet(string)
62+
0006: 02 02 04 C0 F_PUSH ; push string 4, local 0
63+
000a: 17 07 00 branch_when_zero ; 0007 (0012)
64+
000d: 40 00 local ; LV0
65+
000f: 19 05 00 branch ; 0005 (0015)
66+
0012: 44 01 00 global ; name(1)
67+
0015: 6C + ;
68+
0016: 6B 01 (void)assign_local ; LV1
69+
; clone/explorer_sample.lpc:9
70+
0018: 0F const0 ;
71+
0019: 6B 02 (void)assign_local ; LV2
72+
001b: 19 10 00 branch ; 0010 (002c)
73+
001e: 02 02 C2 41 F_PUSH ; push local 2, number 1
74+
0022: 2D 02 00 02 F_CALL_FUNCTION_BY_ADDRESS ; add, pushed_args:2
75+
0026: 45 00 00 global_lvalue ; counter(0)
76+
0029: 69 (void)+= ;
77+
002a: 23 02 loop_incr ; LV2
78+
002c: 22 02 03 00 00 00 00 00 00 00 18 00 loop_cond_number ; LV2 < 3 branch back 0018 (001e)
79+
; clone/explorer_sample.lpc:10
80+
0038: 02 02 80 45 F_PUSH ; push global 0, number 5
81+
003c: 15 05 00 branch_le ; 0005 (0042)
82+
003f: 40 01 local ; LV1
83+
0041: 2F return ;
84+
0042: 0E 05 short_string ; ""
85+
0044: 2F return ;
86+
0045: 30 return_zero ;
87+
; clone/explorer_sample.lpc:11
88+
89+
;; Function: public void create()
90+
0046: 0E 06 short_string ; "world"
91+
0048: 2D 01 00 01 F_CALL_FUNCTION_BY_ADDRESS ; greet, pushed_args:1
92+
004c: 01 pop ;
93+
004d: 30 return_zero ;
94+
; clone/explorer_sample.lpc:16
95+
96+
;; Function: hidden void #global_init#()
97+
004e: 0E 03 short_string ; "sample"
98+
0050: 45 01 00 global_lvalue ; name(1)
99+
0053: 6A (void)assign ;
100+
0054: 30 return_zero ;
101+
; clone/explorer_sample.lpc:4
102+
103+
;;; *** Line Number Info ***
104+
105+
absolute line -> (file, line) table:
106+
0 lines from 1 [clone/explorer_sample.lpc]
107+
11 lines from 2 [include/globals.h]
108+
32 lines from 3 [include/tests.h]
109+
25 lines from 2 [include/globals.h]
110+
18 lines from 1 [clone/explorer_sample.lpc]
111+
112+
address -> absolute line table:
113+
0000-0005: 74
114+
0006-0017: 77
115+
0018-0037: 78
116+
0038-0045: 79
117+
0046-004d: 84
118+
004e-0054: 72
119+
Trace duration: 8381.036000 us, dumping 108 events to trace_lpcc.json in separate thread.
120+
[thread 139992846300864d]: Dump trace successfully to file trace_lpcc.json, cost 0 ms.

scripts/test.mjs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,14 @@ check('lpcc bytecode: instructions carry addr/mnemonic and trailing src annotati
119119
})());
120120
check('lpcc bytecode: address->line table parsed',
121121
pbc.addressLines.length > 0 && pbc.addressLines.every((r) => r.absLine > 0));
122+
// Pins the upstream loop_cond_number decode fix: before it, create() and
123+
// #global_init# were swallowed into greet()'s garbage tail.
124+
check('lpcc bytecode: all four functions disassemble (decode-desync fix)',
125+
pbc.functions.length === 4 && pbc.functions.some((f) => f.name === 'create'));
126+
127+
const pbcO0 = lpccSvc.parseBytecode(fixture('sample.disO0.txt'));
128+
check('lpcc bytecode -O0: parses as a distinct dump',
129+
pbcO0 && pbcO0.functions.length >= 2 && pbcO0.name === pbc.name);
122130

123131
check('lpcc diagnostics: clang-style lines extracted from mixed output',
124132
lpccSvc.parseDiagnostics(fixture('sample.dis.txt')).length === 4);

0 commit comments

Comments
 (0)