Files
TheRock/rocm-libraries/shared/stinkytofu/tools/stinkytofu-cfg/index.html
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

2237 lines
68 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Stinky CFG Viewer</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,600;0,9..40,700;1,9..40,400&family=Fraunces:ital,opsz,wght@0,9..144,600;0,9..144,700;1,9..144,500&display=swap" rel="stylesheet" />
<script src="https://unpkg.com/dagre@0.8.5/dist/dagre.min.js"></script>
<script src="https://unpkg.com/cytoscape@3.30.4/dist/cytoscape.min.js"></script>
<script src="https://unpkg.com/cytoscape-dagre@2.5.0/cytoscape-dagre.js"></script>
<style>
:root {
--bg: #0f1210;
--panel: #171b18;
--border: #2a322c;
--text: #e8ebe6;
--muted: #8a9389;
--accent: #c4e86a;
--node-fill: #ffffff;
--node-stroke: #b8bdc3;
--node-text: #202124;
--node-entry: #1a73e8;
--node-exit: #d93025;
--node-selected: #c4e86a;
--node-highlight: #8ab4f8;
--edge-default: #5f6368;
--edge-fallthrough: #1a73e8;
--edge-highlight: #c4e86a;
--sidebar-bg: #1e2125;
--sidebar-text: #e8eaed;
--sidebar-muted: #9aa0a6;
--sidebar-hover: rgba(255, 255, 255, 0.06);
--sidebar-active: rgba(138, 180, 248, 0.18);
--main-canvas: #f1f3f4;
--main-surface: #ffffff;
--main-text: #202124;
--main-muted: #5f6368;
--main-border: #dadce0;
--search-bg: #fff;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
height: 100vh;
overflow: hidden;
font-family: "DM Sans", system-ui, sans-serif;
background: var(--sidebar-bg);
color: var(--main-text);
}
.app-shell {
display: flex;
height: 100vh;
width: 100vw;
}
/* ---------------- Sidebar ---------------- */
.sidebar {
width: 16rem;
flex-shrink: 0;
background: var(--sidebar-bg);
color: var(--sidebar-text);
display: flex;
flex-direction: column;
border-right: 1px solid rgba(255, 255, 255, 0.08);
min-height: 0;
transition: margin-left 0.2s ease, opacity 0.2s ease;
}
.sidebar.collapsed {
margin-left: -16rem;
opacity: 0;
pointer-events: none;
}
.sidebar-brand {
padding: 1rem 0.9rem 0.75rem;
font-family: Fraunces, Georgia, serif;
font-size: 1.05rem;
font-weight: 600;
letter-spacing: -0.02em;
color: var(--sidebar-text);
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.sidebar-section {
padding: 0.5rem 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.sidebar-item {
display: flex;
align-items: center;
gap: 0.55rem;
width: 100%;
padding: 0.5rem 0.9rem;
border: none;
background: transparent;
color: var(--sidebar-text);
font-family: inherit;
font-size: 0.82rem;
text-align: left;
cursor: pointer;
transition: background 0.12s;
}
.sidebar-item:hover { background: var(--sidebar-hover); }
.sidebar-item .si-icon {
width: 1.1rem;
text-align: center;
opacity: 0.85;
flex-shrink: 0;
}
label.sidebar-item { cursor: pointer; margin: 0; }
label.sidebar-item input[type="file"] { display: none; }
.sidebar-label {
padding: 0.45rem 0.9rem 0.3rem;
color: var(--sidebar-muted);
font-size: 0.65rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.09em;
}
.sidebar-collapse-toggle {
display: flex;
align-items: center;
gap: 0.45rem;
width: 100%;
padding: 0.55rem 0.9rem 0.4rem;
border: none;
background: transparent;
color: var(--sidebar-muted);
font-family: inherit;
font-size: 0.65rem;
font-weight: 600;
text-align: left;
text-transform: uppercase;
letter-spacing: 0.09em;
cursor: pointer;
}
.sidebar-collapse-toggle:hover {
background: var(--sidebar-hover);
color: var(--sidebar-text);
}
.sidebar-collapse-caret {
width: 0.7rem;
flex-shrink: 0;
transition: transform 0.12s ease;
}
.sidebar-section.collapsed .sidebar-collapse-caret {
transform: rotate(-90deg);
}
.sidebar-collapse-title {
flex: 1;
min-width: 0;
}
.sidebar-collapse-count {
flex-shrink: 0;
color: var(--sidebar-muted);
font-size: 0.62rem;
text-transform: none;
letter-spacing: 0;
}
.sidebar-section.collapsed .sidebar-collapsible-body {
display: none;
}
.sidebar-control {
margin: 0 0.9rem 0.5rem;
width: calc(100% - 1.8rem);
padding: 0.4rem 0.55rem;
border: 1px solid rgba(255, 255, 255, 0.12);
background: #0f1114;
color: var(--sidebar-text);
font-family: inherit;
font-size: 0.78rem;
border-radius: 6px;
}
.sidebar-control:focus {
outline: none;
border-color: #8ab4f8;
box-shadow: 0 0 0 2px rgba(138, 180, 248, 0.25);
}
.block-list-wrap {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.block-list-wrap.collapsed {
flex: 0 0 auto;
min-height: auto;
}
.block-list-wrap .sidebar-collapsible-body {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.block-list {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 0.25rem 0;
}
.block-row {
display: flex;
align-items: baseline;
gap: 0.5rem;
padding: 0.3rem 0.9rem;
font-family: ui-monospace, "Cascadia Code", monospace;
font-size: 0.72rem;
color: var(--sidebar-text);
cursor: pointer;
border-left: 3px solid transparent;
line-height: 1.3;
word-break: break-all;
}
.block-row:hover { background: var(--sidebar-hover); }
.block-row.selected {
background: var(--sidebar-active);
border-left-color: #8ab4f8;
}
.block-row .br-line {
color: var(--sidebar-muted);
flex-shrink: 0;
min-width: 2.5rem;
text-align: right;
font-size: 0.65rem;
}
.block-row .br-name {
flex: 1;
min-width: 0;
}
.block-row .br-count {
color: var(--sidebar-muted);
flex-shrink: 0;
font-size: 0.65rem;
}
.block-row.hidden { display: none; }
/* ---------------- Instruction-type filter ---------------- */
.instr-filter-actions {
display: flex;
gap: 0.4rem;
margin: 0 0.9rem 0.45rem;
}
.instr-filter-actions button {
flex: 1;
padding: 0.3rem 0.4rem;
border: 1px solid rgba(255, 255, 255, 0.12);
background: #0f1114;
color: var(--sidebar-text);
font-family: inherit;
font-size: 0.7rem;
border-radius: 6px;
cursor: pointer;
}
.instr-filter-actions button:hover { background: var(--sidebar-hover); }
.instr-filter-list {
max-height: 13rem;
overflow-y: auto;
margin: 0 0.9rem 0.55rem;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;
background: #0f1114;
}
.instr-type-row {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.28rem 0.55rem;
margin: 0;
font-family: ui-monospace, "Cascadia Code", monospace;
font-size: 0.7rem;
color: var(--sidebar-text);
cursor: pointer;
line-height: 1.25;
}
.instr-type-row:hover { background: var(--sidebar-hover); }
.instr-type-row input {
accent-color: #8ab4f8;
cursor: pointer;
flex-shrink: 0;
margin: 0;
}
.instr-type-row .it-name { flex: 1; min-width: 0; word-break: break-all; }
.instr-type-row .it-count {
color: var(--sidebar-muted);
flex-shrink: 0;
font-size: 0.62rem;
}
.instr-type-row.hidden { display: none; }
.instr-filter-empty {
padding: 0.4rem 0.6rem;
color: var(--sidebar-muted);
font-size: 0.68rem;
}
.sidebar-footer {
padding: 0.5rem 0.9rem 0.65rem;
font-size: 0.65rem;
color: var(--sidebar-muted);
border-top: 1px solid rgba(255, 255, 255, 0.06);
}
/* ---------------- Main ---------------- */
.main-wrap {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
background: var(--main-canvas);
color: var(--main-text);
}
.main-topbar {
flex-shrink: 0;
padding: 0.55rem 1rem;
background: #e8eaed;
border-bottom: 1px solid var(--main-border);
display: flex;
gap: 0.65rem;
align-items: center;
}
.hamburger-btn {
background: none;
border: none;
cursor: pointer;
padding: 0.4rem;
display: flex;
flex-direction: column;
gap: 4px;
flex-shrink: 0;
}
.hamburger-btn span {
display: block;
width: 18px;
height: 2px;
background: var(--main-muted);
border-radius: 1px;
transition: background 0.15s;
}
.hamburger-btn:hover span { background: var(--main-text); }
.search-wrap {
flex: 1;
position: relative;
max-width: 42rem;
}
.search-wrap .search-icon {
position: absolute;
left: 0.85rem;
top: 50%;
transform: translateY(-50%);
width: 1rem;
height: 1rem;
opacity: 0.45;
pointer-events: none;
}
.search-wrap input[type="search"] {
width: 100%;
box-sizing: border-box;
padding: 0.5rem 0.85rem 0.5rem 2.35rem;
border-radius: 8px;
border: 1px solid var(--main-border);
background: var(--search-bg);
font-family: inherit;
font-size: 0.88rem;
color: var(--main-text);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.search-wrap input[type="search"]:focus {
outline: none;
border-color: #8ab4f8;
box-shadow: 0 0 0 2px rgba(138, 180, 248, 0.35);
}
.topbar-btn {
padding: 0.45rem 0.75rem;
border-radius: 6px;
border: 1px solid var(--main-border);
background: #fff;
color: var(--main-text);
font-family: inherit;
font-size: 0.8rem;
cursor: pointer;
}
.topbar-btn:hover {
background: #f1f3f4;
border-color: #bdc1c6;
}
.topbar-toggle {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.42rem 0.7rem;
border-radius: 6px;
border: 1px solid var(--main-border);
background: #fff;
color: var(--main-text);
font-family: inherit;
font-size: 0.8rem;
cursor: pointer;
white-space: nowrap;
}
.topbar-toggle:hover {
background: #f1f3f4;
border-color: #bdc1c6;
}
.topbar-toggle input {
margin: 0;
accent-color: #1a73e8;
cursor: pointer;
}
.topbar-status {
color: var(--main-muted);
font-size: 0.78rem;
margin-left: 0.5rem;
white-space: nowrap;
}
.main-body {
flex: 1;
min-height: 0;
display: flex;
}
.editor-panel {
flex-shrink: 0;
height: 16rem;
min-height: 8rem;
max-height: 45vh;
overflow: hidden;
display: flex;
flex-direction: column;
background: #111418;
border-top: 1px solid var(--main-border);
}
.editor-panel.collapsed { display: none; }
.editor-resize-handle {
flex-shrink: 0;
height: 0.45rem;
cursor: ns-resize;
background:
linear-gradient(to right, transparent, rgba(255, 255, 255, 0.18), transparent) center / 7rem 1px no-repeat,
#171b18;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.editor-resize-handle:hover,
.editor-panel.resizing .editor-resize-handle {
background:
linear-gradient(to right, transparent, rgba(196, 232, 106, 0.65), transparent) center / 7rem 2px no-repeat,
#1e2125;
}
body.resizing-editor {
cursor: ns-resize;
user-select: none;
}
.editor-panel-head {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 0.45rem 0.75rem;
background: #1e2125;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
color: var(--sidebar-text);
}
.editor-panel-title {
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--sidebar-muted);
white-space: nowrap;
}
.stir-editor-actions {
display: flex;
gap: 0.4rem;
align-items: center;
flex-shrink: 0;
}
.stir-editor-actions button {
padding: 0.32rem 0.55rem;
border: 1px solid rgba(255, 255, 255, 0.12);
background: #0f1114;
color: var(--sidebar-text);
font-family: inherit;
font-size: 0.72rem;
border-radius: 6px;
cursor: pointer;
}
.stir-editor-actions button:hover { background: rgba(255, 255, 255, 0.06); }
.stir-editor {
flex: 1;
min-height: 0;
width: 100%;
margin: 0;
padding: 0.65rem 0.8rem;
border: none;
outline: none;
resize: none;
background: #0f1114;
color: var(--sidebar-text);
font-family: ui-monospace, "Cascadia Code", monospace;
font-size: 0.78rem;
line-height: 1.45;
white-space: pre;
overflow: auto;
}
.stir-editor-wrap {
position: relative;
flex: 1;
min-height: 0;
background: #0f1114;
overflow: hidden;
}
.stir-editor-highlight,
.stir-editor {
position: absolute;
inset: 0;
tab-size: 2;
}
.stir-editor-highlight {
margin: 0;
padding: 0.65rem 0.8rem;
border: none;
overflow: auto;
color: var(--sidebar-text);
font-family: ui-monospace, "Cascadia Code", monospace;
font-size: 0.78rem;
line-height: 1.45;
white-space: pre;
pointer-events: none;
scrollbar-width: none;
}
.stir-editor-highlight::-webkit-scrollbar { display: none; }
.stir-editor {
background: transparent;
color: transparent;
caret-color: var(--sidebar-text);
-webkit-text-fill-color: transparent;
}
.stir-editor::selection {
background: rgba(138, 180, 248, 0.32);
-webkit-text-fill-color: transparent;
}
.stir-editor::placeholder {
color: #6e7681;
-webkit-text-fill-color: #6e7681;
}
.mlir-comment { color: #7d8590; font-style: italic; }
.mlir-string { color: #a5d6ff; }
.mlir-op { color: #d2a8ff; }
.mlir-label { color: #ffa657; }
.mlir-symbol { color: #79c0ff; }
.mlir-value { color: #ffa657; }
.mlir-keyword { color: #ff7b72; }
.mlir-number { color: #79c0ff; }
.mlir-punct { color: #8b949e; }
.graph-pane {
flex: 1;
min-width: 0;
position: relative;
background:
radial-gradient(circle at 50% 50%, #fafbfc 0%, #eceef1 100%);
}
#cy {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.self-loop-layer {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 3;
overflow: visible;
}
.self-loop-layer path {
fill: none;
stroke: #5f6368;
stroke-width: 1.8;
}
.empty-overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
color: var(--main-muted);
font-size: 0.95rem;
text-align: center;
padding: 2rem;
}
.empty-overlay.hidden { display: none; }
.empty-overlay .empty-card {
background: #fff;
border: 1px dashed var(--main-border);
padding: 1.25rem 1.5rem;
border-radius: 12px;
max-width: 28rem;
pointer-events: auto;
}
.empty-overlay h3 {
margin: 0 0 0.5rem;
font-family: Fraunces, Georgia, serif;
font-weight: 600;
color: var(--main-text);
}
.empty-overlay p {
margin: 0.3rem 0;
font-size: 0.85rem;
}
.drop-overlay {
position: absolute;
inset: 0;
background: rgba(138, 180, 248, 0.18);
border: 3px dashed #1a73e8;
display: none;
align-items: center;
justify-content: center;
font-size: 1.1rem;
color: #1a73e8;
font-weight: 600;
z-index: 50;
pointer-events: none;
}
.drop-overlay.visible { display: flex; }
/* ---------------- Detail pane ---------------- */
.detail-pane {
width: 32rem;
flex-shrink: 0;
background: var(--main-surface);
border-left: 1px solid var(--main-border);
display: flex;
flex-direction: column;
min-height: 0;
transition: width 0.18s ease, margin-right 0.18s ease, opacity 0.18s ease;
}
.detail-pane.collapsed {
width: 0;
margin-right: -32rem;
opacity: 0;
pointer-events: none;
}
.detail-head {
flex-shrink: 0;
padding: 0.85rem 1rem 0.6rem;
border-bottom: 1px solid var(--main-border);
}
.detail-head .dh-row {
display: flex;
align-items: center;
gap: 0.5rem;
}
.detail-head .dh-title {
flex: 1;
min-width: 0;
font-family: ui-monospace, "Cascadia Code", monospace;
font-size: 0.95rem;
font-weight: 600;
color: var(--main-text);
word-break: break-all;
}
.detail-head .dh-close {
flex-shrink: 0;
width: 1.8rem;
height: 1.8rem;
border-radius: 50%;
border: none;
background: transparent;
color: var(--main-muted);
cursor: pointer;
font-size: 1.1rem;
display: flex;
align-items: center;
justify-content: center;
}
.detail-head .dh-close:hover {
background: #f1f3f4;
color: var(--main-text);
}
.detail-head .dh-meta {
color: var(--main-muted);
font-size: 0.75rem;
margin-top: 0.25rem;
}
.detail-section {
padding: 0.65rem 1rem 0.25rem;
}
.detail-section h4 {
margin: 0 0 0.35rem;
font-size: 0.68rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--main-muted);
font-weight: 600;
}
.pill-row {
display: flex;
flex-wrap: wrap;
gap: 0.3rem;
}
.pill {
display: inline-flex;
align-items: center;
padding: 0.18rem 0.55rem;
border-radius: 999px;
background: #e8f0fe;
color: #1a73e8;
border: 1px solid #d2e3fc;
font-size: 0.72rem;
font-family: ui-monospace, "Cascadia Code", monospace;
cursor: pointer;
max-width: 100%;
word-break: break-all;
}
.pill:hover {
background: #d2e3fc;
}
.pill.muted {
background: #f8f9fa;
color: var(--main-muted);
border-color: var(--main-border);
cursor: default;
}
.pill.muted:hover { background: #f8f9fa; }
.instructions-wrap {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
padding: 0.4rem 1rem 1rem;
overflow: hidden;
}
.instructions-header {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.35rem;
}
.instructions-header h4 {
margin: 0;
font-size: 0.68rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--main-muted);
font-weight: 600;
}
.copy-btn {
padding: 0.25rem 0.55rem;
border: 1px solid var(--main-border);
border-radius: 5px;
background: #fff;
color: var(--main-muted);
font-family: inherit;
font-size: 0.72rem;
cursor: pointer;
}
.copy-btn:hover {
color: var(--main-text);
background: #f8f9fa;
}
.instructions-pre {
flex: 1;
min-height: 0;
margin: 0;
padding: 0.6rem 0.75rem;
background: #fafafa;
border: 1px solid var(--main-border);
border-radius: 6px;
overflow: auto;
font-family: ui-monospace, "Cascadia Code", monospace;
font-size: 0.74rem;
line-height: 1.5;
color: var(--main-text);
white-space: pre;
}
.instructions-pre .il-line {
display: block;
}
.instructions-pre .il-line.il-match {
background: rgba(196, 232, 106, 0.32);
border-radius: 3px;
}
.instructions-pre .il-num {
display: inline-block;
min-width: 3.25rem;
padding-right: 0.65rem;
color: var(--main-muted);
text-align: right;
user-select: none;
}
@media (max-width: 1100px) {
.detail-pane { width: 24rem; }
.detail-pane.collapsed { margin-right: -24rem; }
}
@media (max-width: 820px) {
.sidebar { width: 13rem; }
.sidebar.collapsed { margin-left: -13rem; }
.detail-pane { width: 20rem; }
.detail-pane.collapsed { margin-right: -20rem; }
}
</style>
</head>
<body>
<div class="app-shell">
<aside class="sidebar" aria-label="Main menu">
<div class="sidebar-brand">Stinky CFG Viewer</div>
<div class="sidebar-section">
<label class="sidebar-item" title="Open a .stir file">
<span class="si-icon" aria-hidden="true">📂</span>
Open .stir file
<input type="file" id="fileStir" accept=".stir,.txt,text/plain" />
</label>
</div>
<div class="sidebar-section">
<div class="sidebar-label">Layout</div>
<select id="layoutSelect" class="sidebar-control" title="Graph layout direction">
<option value="TB" selected>Top → Bottom (dagre)</option>
<option value="LR">Left → Right (dagre)</option>
<option value="BT">Bottom → Top (dagre)</option>
<option value="RL">Right → Left (dagre)</option>
</select>
</div>
<div class="sidebar-section collapsed" id="instrFilterSection" hidden>
<button type="button" class="sidebar-collapse-toggle" id="instrFilterToggle" aria-controls="instrFilterBody" aria-expanded="false">
<span class="sidebar-collapse-caret" aria-hidden="true">▾</span>
<span class="sidebar-collapse-title">Filter by instruction type</span>
<span class="sidebar-collapse-count" id="instrFilterCount"></span>
</button>
<div class="sidebar-collapsible-body" id="instrFilterBody">
<input
type="search"
id="instrTypeFilter"
class="sidebar-control"
placeholder="Filter types…"
autocomplete="off"
spellcheck="false"
/>
<div class="instr-filter-actions">
<button type="button" id="instrSelectAll" title="Select all visible types">Select all</button>
<button type="button" id="instrClear" title="Clear selection">Clear</button>
</div>
<div class="instr-filter-list" id="instrTypeList" role="group" aria-label="Instruction types"></div>
</div>
</div>
<div class="sidebar-section block-list-wrap">
<button type="button" class="sidebar-collapse-toggle" id="blockListToggle" aria-controls="blockListBody" aria-expanded="true">
<span class="sidebar-collapse-caret" aria-hidden="true">▾</span>
<span class="sidebar-collapse-title">Basic blocks</span>
<span class="sidebar-collapse-count" id="blockListCount"></span>
</button>
<div class="sidebar-collapsible-body" id="blockListBody">
<input
type="search"
id="blockFilter"
class="sidebar-control"
placeholder="Filter by label…"
autocomplete="off"
spellcheck="false"
/>
<div class="block-list" id="blockList" role="listbox" aria-label="Basic blocks"></div>
</div>
</div>
<div class="sidebar-footer">stinkytofu-cfg · local viewer</div>
</aside>
<div class="main-wrap">
<header class="main-topbar">
<button type="button" class="hamburger-btn" id="sidebarToggle" title="Toggle sidebar" aria-label="Toggle sidebar">
<span></span><span></span><span></span>
</button>
<div class="search-wrap">
<svg class="search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<circle cx="11" cy="11" r="7" />
<path d="M20 20l-4-4" stroke-linecap="round" />
</svg>
<input
type="search"
id="labelSearch"
placeholder="Jump to ^label… (Enter to focus)"
autocomplete="off"
spellcheck="false"
aria-label="Search basic block by label"
list="labelOptions"
/>
<datalist id="labelOptions"></datalist>
</div>
<button type="button" class="topbar-btn" id="btnFit" title="Fit graph in view">Fit</button>
<button type="button" class="topbar-btn" id="btnReset" title="Re-run layout">Re-layout</button>
<button type="button" class="topbar-btn" id="btnToggleEditor" title="Show or hide the STIR editor" aria-controls="editorPanel" aria-expanded="true">Hide editor</button>
<label class="topbar-toggle" title="Show block instructions inside graph nodes">
<input type="checkbox" id="showInstrInNodes" />
Instructions in nodes
</label>
<span class="topbar-status" id="status"></span>
</header>
<div class="main-body">
<div class="graph-pane" id="graphPane">
<div id="cy"></div>
<svg class="self-loop-layer" id="selfLoopLayer" aria-hidden="true">
<defs>
<marker id="selfLoopArrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#5f6368"></path>
</marker>
<marker id="selfLoopArrowHighlight" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#c4e86a"></path>
</marker>
</defs>
<g id="selfLoopPaths"></g>
</svg>
<div class="empty-overlay" id="emptyOverlay">
<div class="empty-card">
<h3>No graph loaded</h3>
<p>Drag &amp; drop a <code>.stir</code> file anywhere on this pane,</p>
<p>or click <strong>Open .stir file</strong> in the sidebar,</p>
<p>or paste STIR into the editor and click <strong>Render editor</strong>.</p>
</div>
</div>
<div class="drop-overlay" id="dropOverlay">Drop .stir file to load</div>
</div>
<aside class="detail-pane collapsed" id="detailPane" aria-label="Block details">
<div class="detail-head">
<div class="dh-row">
<div class="dh-title" id="dhTitle">—</div>
<button type="button" class="dh-close" id="dhClose" aria-label="Close details">×</button>
</div>
<div class="dh-meta" id="dhMeta"></div>
</div>
<div class="detail-section">
<h4>Predecessors</h4>
<div class="pill-row" id="predRow"></div>
</div>
<div class="detail-section">
<h4>Successors</h4>
<div class="pill-row" id="succRow"></div>
</div>
<div class="instructions-wrap">
<div class="instructions-header">
<h4>Instructions</h4>
<button type="button" class="copy-btn" id="copyBtn" title="Copy instructions to clipboard">Copy</button>
</div>
<pre class="instructions-pre" id="instructionsPre"></pre>
</div>
</aside>
</div>
<section class="editor-panel" id="editorPanel" aria-label="STIR editor">
<div class="editor-resize-handle" id="editorResizeHandle" title="Drag to resize STIR editor"></div>
<div class="editor-panel-head">
<div class="editor-panel-title">Edit STIR</div>
<div class="stir-editor-actions">
<button type="button" id="btnLoadEditor" title="Parse the STIR text editor contents">Render editor</button>
<button type="button" id="btnClearEditor" title="Clear the STIR text editor">Clear</button>
</div>
</div>
<div class="stir-editor-wrap">
<pre class="stir-editor-highlight" id="stirEditorHighlight" aria-hidden="true"></pre>
<textarea
id="stirEditor"
class="stir-editor"
placeholder="Paste or edit STIR here…"
autocomplete="off"
spellcheck="false"
></textarea>
</div>
</section>
</div>
</div>
<script>
(function () {
"use strict";
// -------------------- DOM refs --------------------
const els = {
fileStir: document.getElementById("fileStir"),
stirEditor: document.getElementById("stirEditor"),
stirEditorHighlight: document.getElementById("stirEditorHighlight"),
btnLoadEditor: document.getElementById("btnLoadEditor"),
btnClearEditor: document.getElementById("btnClearEditor"),
sidebarToggle: document.getElementById("sidebarToggle"),
layoutSelect: document.getElementById("layoutSelect"),
instrFilterSection: document.getElementById("instrFilterSection"),
instrFilterToggle: document.getElementById("instrFilterToggle"),
instrFilterCount: document.getElementById("instrFilterCount"),
instrTypeFilter: document.getElementById("instrTypeFilter"),
instrTypeList: document.getElementById("instrTypeList"),
instrSelectAll: document.getElementById("instrSelectAll"),
instrClear: document.getElementById("instrClear"),
blockListToggle: document.getElementById("blockListToggle"),
blockListCount: document.getElementById("blockListCount"),
blockFilter: document.getElementById("blockFilter"),
blockList: document.getElementById("blockList"),
labelSearch: document.getElementById("labelSearch"),
labelOptions: document.getElementById("labelOptions"),
btnFit: document.getElementById("btnFit"),
btnReset: document.getElementById("btnReset"),
btnToggleEditor: document.getElementById("btnToggleEditor"),
showInstrInNodes: document.getElementById("showInstrInNodes"),
status: document.getElementById("status"),
graphPane: document.getElementById("graphPane"),
editorPanel: document.getElementById("editorPanel"),
editorResizeHandle: document.getElementById("editorResizeHandle"),
cy: document.getElementById("cy"),
selfLoopLayer: document.getElementById("selfLoopLayer"),
selfLoopPaths: document.getElementById("selfLoopPaths"),
emptyOverlay: document.getElementById("emptyOverlay"),
dropOverlay: document.getElementById("dropOverlay"),
detailPane: document.getElementById("detailPane"),
dhTitle: document.getElementById("dhTitle"),
dhMeta: document.getElementById("dhMeta"),
dhClose: document.getElementById("dhClose"),
predRow: document.getElementById("predRow"),
succRow: document.getElementById("succRow"),
instructionsPre: document.getElementById("instructionsPre"),
copyBtn: document.getElementById("copyBtn"),
};
let cy = null;
let blocks = [];
let byLabel = new Map();
let predsOf = new Map();
let selectedId = null;
let instrTypeCounts = new Map(); // opcode -> total occurrences across all blocks
let selectedTypes = new Set(); // opcodes currently enabled in the filter
let showInstructionsInNodes = false;
function setStatus(text) { els.status.textContent = text || ""; }
function escapeHtml(s) {
return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
function isCompactSidebar() {
return document.querySelector(".sidebar").getBoundingClientRect().height < 760;
}
function setSidebarSectionCollapsed(section, toggle, collapsed) {
section.classList.toggle("collapsed", collapsed);
toggle.setAttribute("aria-expanded", collapsed ? "false" : "true");
}
function toggleLargeSidebarSection(sectionName) {
const isInstr = sectionName === "instr";
const section = isInstr ? els.instrFilterSection : els.blockList.closest(".sidebar-section");
const toggle = isInstr ? els.instrFilterToggle : els.blockListToggle;
const collapsed = section.classList.contains("collapsed");
setSidebarSectionCollapsed(section, toggle, !collapsed);
if (collapsed && isCompactSidebar()) {
if (isInstr) setSidebarSectionCollapsed(els.blockList.closest(".sidebar-section"), els.blockListToggle, true);
else if (!els.instrFilterSection.hidden) setSidebarSectionCollapsed(els.instrFilterSection, els.instrFilterToggle, true);
}
}
function enforceCompactSidebarLists() {
if (!isCompactSidebar()) return;
const instrOpen = !els.instrFilterSection.hidden && !els.instrFilterSection.classList.contains("collapsed");
const blockSection = els.blockList.closest(".sidebar-section");
const blockOpen = !blockSection.classList.contains("collapsed");
if (instrOpen && blockOpen) setSidebarSectionCollapsed(els.instrFilterSection, els.instrFilterToggle, true);
}
function spanToken(cls, text) {
return '<span class="' + cls + '">' + escapeHtml(text) + '</span>';
}
function highlightMlirLine(line) {
let out = "";
let i = 0;
while (i < line.length) {
const rest = line.slice(i);
if (rest.startsWith("//")) {
out += spanToken("mlir-comment", rest);
break;
}
const str = rest.match(/^"([^"\\]|\\.)*"/);
if (str) {
const cls = /^"st\./.test(str[0]) ? "mlir-op" : "mlir-string";
out += spanToken(cls, str[0]);
i += str[0].length;
continue;
}
const label = rest.match(/^\^[A-Za-z_][\w.]*/);
if (label) {
out += spanToken("mlir-label", label[0]);
i += label[0].length;
continue;
}
const symbol = rest.match(/^[@#][A-Za-z_][\w.$-]*/);
if (symbol) {
out += spanToken("mlir-symbol", symbol[0]);
i += symbol[0].length;
continue;
}
const value = rest.match(/^%[A-Za-z_0-9][\w.$-]*/);
if (value) {
out += spanToken("mlir-value", value[0]);
i += value[0].length;
continue;
}
const keyword = rest.match(/^(?:st\.[A-Za-z_][\w.$-]*|Successors|module|func|return|memref|tensor|vector|index|i\d+|f\d+)\b/);
if (keyword) {
out += spanToken("mlir-keyword", keyword[0]);
i += keyword[0].length;
continue;
}
const num = rest.match(/^-?(?:0x[0-9a-fA-F]+|\d+(?:\.\d+)?)/);
if (num) {
out += spanToken("mlir-number", num[0]);
i += num[0].length;
continue;
}
const punct = rest.match(/^[()[\]{}=,:]/);
if (punct) {
out += spanToken("mlir-punct", punct[0]);
i += punct[0].length;
continue;
}
out += escapeHtml(line[i]);
i++;
}
return out;
}
function updateStirEditorHighlight() {
const text = els.stirEditor.value || "";
els.stirEditorHighlight.innerHTML = text
? text.split(/\n/).map(highlightMlirLine).join("\n")
: "";
syncStirEditorScroll();
}
function syncStirEditorScroll() {
els.stirEditorHighlight.scrollTop = els.stirEditor.scrollTop;
els.stirEditorHighlight.scrollLeft = els.stirEditor.scrollLeft;
}
// -------------------- Parser --------------------
// A .stir block looks like:
// ^label_name:
// <instructions...>
// Successors: ^a, ^b
// The function ends with a closing `}` at column 0.
const LABEL_RE = /^\^([A-Za-z_][\w]*):\s*$/;
const SUCC_RE = /^\s*Successors:\s*(.*)$/;
function parseStir(text) {
const out = [];
const byId = new Map();
let cur = null;
let funcOpen = false;
const lines = text.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const ln = lines[i];
if (!funcOpen) {
// Detect any `st.func ... {` line; once we see the open brace, start tracking.
if (/\bst\.func\b/.test(ln) || ln.trim().endsWith("{")) {
funcOpen = true;
}
}
const m = ln.match(LABEL_RE);
if (m) {
if (cur) cur.endLine = i; // previous block ends just before this label
cur = {
id: m[1],
label: m[1],
startLine: i + 1, // 1-based, points at the ^label: line
endLine: i + 1,
instructions: [],
successors: [],
};
out.push(cur);
byId.set(cur.id, cur);
continue;
}
if (!cur) continue; // header / pre-function noise
if (ln.trim() === "}") {
cur.endLine = i; // close-brace line
cur = null;
funcOpen = false;
continue;
}
const s = ln.match(SUCC_RE);
if (s) {
cur.successors = s[1]
.split(",")
.map(t => t.trim().replace(/^\^/, ""))
.filter(Boolean);
} else {
cur.instructions.push(ln);
}
cur.endLine = i + 1;
}
// Trim trailing blank lines per block for prettier display
for (const b of out) {
while (b.instructions.length && b.instructions[b.instructions.length - 1].trim() === "") {
b.instructions.pop();
}
}
return { blocks: out, byLabel: byId };
}
function buildPredMap(blocks) {
const preds = new Map();
for (const b of blocks) {
for (const s of b.successors) {
if (!preds.has(s)) preds.set(s, []);
preds.get(s).push(b.id);
}
}
return preds;
}
// The instruction "type" (opcode) is the first quoted token on the line,
// e.g. `s0 = "st.s_mov_b32"(0)` -> st.s_mov_b32
function extractOpcode(line) {
const m = String(line).match(/"([^"]+)"/);
return m ? m[1] : null;
}
// Annotate each block with a parallel `opcodes` array and tally a global
// map of opcode -> occurrence count for the filter UI.
function indexOpcodes(blocks) {
const counts = new Map();
for (const b of blocks) {
b.opcodes = b.instructions.map(extractOpcode);
for (const op of b.opcodes) {
if (!op) continue;
counts.set(op, (counts.get(op) || 0) + 1);
}
}
return counts;
}
// How many instructions in a block match the currently selected types.
function matchedCount(b) {
if (!selectedTypes.size || !b.opcodes) return 0;
let n = 0;
for (const op of b.opcodes) if (op && selectedTypes.has(op)) n++;
return n;
}
function instructionLabelForNode(line) {
const s = String(line);
let depth = 0;
for (let i = s.length - 1; i >= 0; i--) {
const ch = s[i];
if (ch === "}") depth++;
else if (ch === "{") {
depth--;
if (depth === 0) return s.slice(0, i).trim();
}
}
return s.trim();
}
function keepLineUnwrapped(s) {
return String(s).replace(/ /g, "\u00a0");
}
function instructionLabelsForNode(b) {
const lines = selectedTypes.size
? b.instructions.filter((_, idx) => {
const op = b.opcodes ? b.opcodes[idx] : null;
return op && selectedTypes.has(op);
})
: b.instructions;
return lines.map(instructionLabelForNode);
}
// -------------------- Graph render --------------------
// Node caption: block name + instruction count, plus a match line when an
// instruction-type filter is active so counts show on the basic block nodes.
function nodeLabelFor(b) {
const count = b.instructions.length;
let lbl = "^" + b.id + " (" + count + (count === 1 ? " instr" : " instrs");
if (selectedTypes.size) {
const m = matchedCount(b);
lbl += ", " + m + " match" + (m === 1 ? "" : "es");
}
lbl += ")";
lbl = keepLineUnwrapped(lbl);
if (showInstructionsInNodes) {
const instructionLines = instructionLabelsForNode(b);
const instrIndent = "\u00a0\u00a0\u00a0\u00a0";
if (instructionLines.length) lbl += "\n" + instructionLines.map(line => instrIndent + keepLineUnwrapped(line)).join("\n");
else if (!selectedTypes.size) lbl += "\n" + instrIndent + keepLineUnwrapped("(no instructions)");
}
return lbl;
}
function buildElements() {
const nodes = blocks.map(b => {
const count = b.instructions.length;
return {
data: {
id: b.id,
label: nodeLabelFor(b),
count: count,
isEntry: (predsOf.get(b.id) || []).length === 0,
isExit: b.successors.length === 0,
},
};
});
const edges = [];
let edgeIdx = 0;
for (const b of blocks) {
b.successors.forEach((sid, idx) => {
if (!byLabel.has(sid)) return; // skip dangling refs
const selfLoop = sid === b.id;
edges.push({
data: {
id: `e${edgeIdx++}`,
source: b.id,
target: sid,
fallthrough: !selfLoop && idx === 0 && b.successors.length > 1,
selfLoop: selfLoop,
},
});
});
}
return { nodes, edges };
}
// Cytoscape stylesheets don't resolve CSS variables, so colors are inlined here.
function styleSheet() {
return [
{
selector: "node",
style: {
"background-color": "#ffffff",
"border-width": 1.5,
"border-color": "#b8bdc3",
"shape": "round-rectangle",
"label": "data(label)",
"text-valign": "center",
"text-halign": "center",
"color": "#202124",
"font-family": "ui-monospace, 'Cascadia Code', monospace",
"font-size": 12,
"line-height": 1.35,
"padding": "10px",
"text-wrap": "wrap",
"text-justification": "left",
"text-max-width": 4000,
"width": "label",
"height": "label",
},
},
{
selector: "node[?isEntry]",
style: {
"border-color": "#1a73e8",
"border-width": 3,
},
},
{
selector: "node[?isExit]",
style: {
"border-color": "#d93025",
"border-width": 3,
},
},
{
selector: "node.selected-node",
style: {
"border-color": "#c4e86a",
"border-width": 4,
"background-color": "#fffbe6",
},
},
{
selector: "node.highlighted",
style: {
"border-color": "#8ab4f8",
"border-width": 3,
"background-color": "#eaf2ff",
},
},
{
selector: "node.dim",
style: {
"opacity": 0.25,
},
},
{
selector: "node.filter-match",
style: {
"border-color": "#7cb342",
"border-width": 4,
"background-color": "#f1f8e0",
},
},
{
selector: "node.filter-dim",
style: {
"opacity": 0.18,
},
},
{
selector: "edge",
style: {
"curve-style": "bezier",
"target-arrow-shape": "triangle",
"line-color": "#5f6368",
"target-arrow-color": "#5f6368",
"width": 1.5,
"arrow-scale": 1.1,
},
},
{
selector: "edge[?fallthrough]",
style: {
"line-color": "#1a73e8",
"target-arrow-color": "#1a73e8",
"width": 2,
},
},
{
selector: "edge[?selfLoop]",
style: {
"curve-style": "bezier",
"loop-direction": "270deg",
"loop-sweep": "80deg",
"line-color": "#5f6368",
"target-arrow-color": "#5f6368",
"width": 1.8,
"opacity": 0,
"z-index": 998,
},
},
{
selector: "edge.highlighted",
style: {
"line-color": "#c4e86a",
"target-arrow-color": "#c4e86a",
"width": 3,
"z-index": 999,
},
},
{
selector: "edge.dim",
style: {
"opacity": 0.1,
},
},
];
}
function layoutOpts() {
return {
name: "dagre",
rankDir: els.layoutSelect.value || "TB",
nodeSep: 38,
rankSep: 70,
edgeSep: 14,
animate: false,
fit: true,
padding: 30,
};
}
function renderGraph() {
const { nodes, edges } = buildElements();
if (cy) { cy.destroy(); cy = null; }
cy = cytoscape({
container: els.cy,
elements: { nodes, edges },
style: styleSheet(),
layout: layoutOpts(),
wheelSensitivity: 0.25,
minZoom: 0.05,
maxZoom: 4,
});
cy.on("tap", "node", evt => {
const id = evt.target.id();
selectBlock(id, { center: false });
});
cy.on("tap", evt => {
if (evt.target === cy) {
// background tap: clear selection
clearGraphHighlight();
els.detailPane.classList.add("collapsed");
markBlockListSelection(null);
selectedId = null;
}
});
cy.on("mouseover", "node", evt => {
applyHoverHighlight(evt.target);
});
cy.on("mouseout", "node", () => {
clearGraphHighlight();
if (selectedId) applySelectionHighlight(selectedId);
});
cy.on("render pan zoom resize position layoutstop", syncSelfLoopOverlay);
syncSelfLoopOverlay();
els.emptyOverlay.classList.add("hidden");
}
function syncSelfLoopOverlay() {
if (!cy) {
els.selfLoopPaths.innerHTML = "";
return;
}
const frag = document.createDocumentFragment();
cy.edges("[?selfLoop]").forEach(edge => {
const node = edge.source();
if (!node || node.empty()) return;
const pos = node.renderedPosition();
const w = node.renderedWidth();
const h = node.renderedHeight();
const x = pos.x + w / 2;
const y = pos.y;
const loop = Math.max(34, Math.min(90, h * 0.85));
const inset = Math.min(22, Math.max(10, h * 0.25));
const startY = y - inset;
const endY = y + inset;
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute(
"d",
`M ${x} ${startY} C ${x + loop} ${startY - loop * 0.45}, ${x + loop} ${endY + loop * 0.45}, ${x} ${endY}`
);
const highlighted = edge.hasClass("highlighted") || node.hasClass("highlighted") || node.hasClass("selected-node");
path.setAttribute("stroke", highlighted ? "#c4e86a" : "#5f6368");
path.setAttribute("stroke-width", highlighted ? "3" : "1.8");
path.setAttribute("marker-end", highlighted ? "url(#selfLoopArrowHighlight)" : "url(#selfLoopArrow)");
path.setAttribute("opacity", node.hasClass("dim") || node.hasClass("filter-dim") ? "0.18" : "1");
frag.appendChild(path);
});
els.selfLoopPaths.replaceChildren(frag);
}
function refreshNodeLabels(rerunLayout) {
if (!cy) return;
cy.batch(() => {
cy.nodes().forEach(node => {
const b = byLabel.get(node.id());
if (b) node.data("label", nodeLabelFor(b));
});
});
if (rerunLayout) {
cy.layout(layoutOpts()).run();
requestAnimationFrame(syncSelfLoopOverlay);
} else {
syncSelfLoopOverlay();
}
}
function clearGraphHighlight() {
if (!cy) return;
cy.elements().removeClass("highlighted dim selected-node");
syncSelfLoopOverlay();
}
function applyHoverHighlight(node) {
if (!cy) return;
cy.elements().addClass("dim");
const neigh = node.closedNeighborhood();
neigh.removeClass("dim").addClass("highlighted");
syncSelfLoopOverlay();
}
function applySelectionHighlight(id) {
if (!cy) return;
const node = cy.getElementById(id);
if (!node || node.empty()) return;
cy.elements().removeClass("highlighted dim selected-node");
cy.elements().addClass("dim");
const neigh = node.closedNeighborhood();
neigh.removeClass("dim").addClass("highlighted");
node.removeClass("highlighted").addClass("selected-node");
syncSelfLoopOverlay();
}
function focusNode(id) {
if (!cy) return;
const node = cy.getElementById(id);
if (!node || node.empty()) return;
cy.animate({
center: { eles: node },
zoom: Math.max(cy.zoom(), 0.85),
duration: 280,
easing: "ease-out",
});
}
// -------------------- Detail pane --------------------
function showDetail(id) {
const b = byLabel.get(id);
if (!b) return;
els.dhTitle.textContent = "^" + b.id;
const preds = predsOf.get(b.id) || [];
const succs = b.successors || [];
els.dhMeta.textContent =
`line ${b.startLine}` +
(b.endLine && b.endLine !== b.startLine ? `–${b.endLine}` : "") +
` · ${b.instructions.length} instr · ${preds.length} pred · ${succs.length} succ`;
renderPills(els.predRow, preds);
renderPills(els.succRow, succs);
const instrHtml = b.instructions.map((ln, idx) => {
const lineNum = b.startLine + 1 + idx; // first instruction is one after label
const op = b.opcodes ? b.opcodes[idx] : null;
const isMatch = selectedTypes.size && op && selectedTypes.has(op);
return (
'<span class="il-line' + (isMatch ? " il-match" : "") + '">' +
'<span class="il-num">' + lineNum + '</span>' +
escapeHtml(ln) +
'</span>'
);
}).join("");
els.instructionsPre.innerHTML = instrHtml || '<span class="il-line"><em>(no instructions)</em></span>';
els.instructionsPre.scrollTop = 0;
els.detailPane.classList.remove("collapsed");
}
function renderPills(container, ids) {
if (!ids || !ids.length) {
container.innerHTML = '<span class="pill muted">—</span>';
return;
}
container.innerHTML = "";
for (const id of ids) {
const exists = byLabel.has(id);
const pill = document.createElement(exists ? "button" : "span");
pill.className = "pill" + (exists ? "" : " muted");
pill.textContent = "^" + id;
pill.title = exists ? "Jump to ^" + id : "Unknown label";
if (exists) {
pill.type = "button";
pill.addEventListener("click", () => selectBlock(id, { center: true }));
}
container.appendChild(pill);
}
}
function copyInstructions() {
if (!selectedId) return;
const b = byLabel.get(selectedId);
if (!b) return;
const text =
"^" + b.id + ":\n" +
b.instructions.join("\n") +
(b.successors.length ? "\n Successors: " + b.successors.map(s => "^" + s).join(", ") : "");
navigator.clipboard.writeText(text).then(
() => { setStatus("Copied " + b.instructions.length + " lines"); setTimeout(() => setStatus(""), 1500); },
err => { setStatus("Copy failed: " + (err && err.message || err)); }
);
}
// -------------------- Sidebar block list & search --------------------
function renderBlockList() {
els.blockList.innerHTML = "";
const frag = document.createDocumentFragment();
els.labelOptions.innerHTML = "";
const optFrag = document.createDocumentFragment();
for (const b of blocks) {
const row = document.createElement("div");
row.className = "block-row";
row.dataset.id = b.id;
row.setAttribute("role", "option");
row.innerHTML =
'<span class="br-line">' + b.startLine + '</span>' +
'<span class="br-name">^' + escapeHtml(b.id) + '</span>' +
'<span class="br-count">' + b.instructions.length + '</span>';
row.addEventListener("click", () => selectBlock(b.id, { center: true }));
frag.appendChild(row);
const opt = document.createElement("option");
opt.value = "^" + b.id;
optFrag.appendChild(opt);
}
els.blockList.appendChild(frag);
els.labelOptions.appendChild(optFrag);
els.blockListCount.textContent = blocks.length ? String(blocks.length) : "";
applyBlockFilter();
}
function applyBlockFilter() {
const q = (els.blockFilter.value || "").trim().toLowerCase();
const rows = els.blockList.querySelectorAll(".block-row");
let shown = 0;
rows.forEach(row => {
const id = (row.dataset.id || "").toLowerCase();
const hit = !q || id.includes(q);
row.classList.toggle("hidden", !hit);
if (hit) shown++;
});
els.blockListCount.textContent = q ? shown + "/" + rows.length : (rows.length ? String(rows.length) : "");
if (q) setStatus(shown + " / " + rows.length + " blocks match");
else setStatus("");
}
function markBlockListSelection(id) {
const rows = els.blockList.querySelectorAll(".block-row");
rows.forEach(row => row.classList.toggle("selected", row.dataset.id === id));
if (id) {
const r = els.blockList.querySelector('.block-row[data-id="' + cssEscape(id) + '"]');
if (r) r.scrollIntoView({ block: "nearest" });
}
}
function cssEscape(s) {
if (window.CSS && CSS.escape) return CSS.escape(s);
return String(s).replace(/[^a-zA-Z0-9_-]/g, ch => "\\" + ch.charCodeAt(0).toString(16) + " ");
}
// -------------------- Instruction-type filter --------------------
function renderInstrFilter() {
const list = els.instrTypeList;
list.innerHTML = "";
const types = Array.from(instrTypeCounts.keys()).sort();
if (!types.length) {
els.instrFilterSection.hidden = true;
els.instrFilterCount.textContent = "";
return;
}
els.instrFilterSection.hidden = false;
els.instrFilterCount.textContent = String(types.length);
const frag = document.createDocumentFragment();
for (const t of types) {
const row = document.createElement("label");
row.className = "instr-type-row";
row.dataset.type = t.toLowerCase();
row.title = t + " · " + instrTypeCounts.get(t) + " occurrence(s)";
const cb = document.createElement("input");
cb.type = "checkbox";
cb.value = t;
cb.checked = selectedTypes.has(t);
cb.addEventListener("change", () => {
if (cb.checked) selectedTypes.add(t);
else selectedTypes.delete(t);
applyInstrFilter();
});
const name = document.createElement("span");
name.className = "it-name";
name.textContent = t;
const cnt = document.createElement("span");
cnt.className = "it-count";
cnt.textContent = instrTypeCounts.get(t);
row.appendChild(cb);
row.appendChild(name);
row.appendChild(cnt);
frag.appendChild(row);
}
list.appendChild(frag);
applyInstrTypeSearch();
enforceCompactSidebarLists();
}
function applyInstrTypeSearch() {
const q = (els.instrTypeFilter.value || "").trim().toLowerCase();
const rows = els.instrTypeList.querySelectorAll(".instr-type-row");
rows.forEach(r => {
const hit = !q || (r.dataset.type || "").includes(q);
r.classList.toggle("hidden", !hit);
});
}
// Toggle every checkbox currently visible (i.e. respecting the type search).
function setVisibleTypes(select) {
const rows = els.instrTypeList.querySelectorAll(".instr-type-row:not(.hidden)");
rows.forEach(r => {
const cb = r.querySelector("input");
if (!cb) return;
cb.checked = select;
if (select) selectedTypes.add(cb.value);
else selectedTypes.delete(cb.value);
});
applyInstrFilter();
}
function clearTypeSelection() {
selectedTypes.clear();
els.instrTypeList.querySelectorAll(".instr-type-row input").forEach(cb => { cb.checked = false; });
applyInstrFilter();
}
// Reflect the selected instruction types on the graph: matched nodes get an
// accent border + match count in their label, others are dimmed.
function applyInstrFilter() {
const active = selectedTypes.size > 0;
if (cy) {
cy.batch(() => {
cy.nodes().forEach(node => {
const b = byLabel.get(node.id());
if (!b) return;
node.data("label", nodeLabelFor(b));
node.removeClass("filter-match filter-dim");
if (active) {
if (matchedCount(b) > 0) node.addClass("filter-match");
else node.addClass("filter-dim");
}
});
});
}
if (active) {
const hits = blocks.reduce((n, b) => n + (matchedCount(b) > 0 ? 1 : 0), 0);
els.instrFilterCount.textContent = selectedTypes.size + "/" + instrTypeCounts.size;
setStatus(hits + " / " + blocks.length + " blocks contain selected type" + (selectedTypes.size === 1 ? "" : "s"));
} else {
els.instrFilterCount.textContent = instrTypeCounts.size ? String(instrTypeCounts.size) : "";
setStatus("");
}
if (selectedId) showDetail(selectedId);
}
// -------------------- Top-level select / focus --------------------
function selectBlock(id, opts) {
const o = opts || {};
if (!byLabel.has(id)) return;
selectedId = id;
applySelectionHighlight(id);
showDetail(id);
markBlockListSelection(id);
if (o.center) focusNode(id);
}
function jumpToSearch() {
const raw = (els.labelSearch.value || "").trim();
if (!raw) return;
const norm = raw.replace(/^\^+/, "");
if (byLabel.has(norm)) {
selectBlock(norm, { center: true });
return;
}
// substring search across labels (first match)
const lc = norm.toLowerCase();
const hit = blocks.find(b => b.id.toLowerCase().includes(lc));
if (hit) selectBlock(hit.id, { center: true });
else setStatus('No block matches "' + raw + '"');
}
// -------------------- Ingest --------------------
function ingestText(text, label) {
setStatus("Parsing…");
// Defer to next frame so the status update paints before the heavy work.
requestAnimationFrame(() => {
let parsed;
const t0 = performance.now();
try {
parsed = parseStir(text);
} catch (e) {
setStatus("Parse error: " + (e && e.message || e));
return;
}
blocks = parsed.blocks;
byLabel = parsed.byLabel;
predsOf = buildPredMap(blocks);
instrTypeCounts = indexOpcodes(blocks);
selectedTypes = new Set();
els.instrTypeFilter.value = "";
if (!blocks.length) {
setStatus("No basic blocks found in " + (label || "file"));
els.blockList.innerHTML = "";
els.labelOptions.innerHTML = "";
els.instrTypeList.innerHTML = "";
els.blockListCount.textContent = "";
els.instrFilterCount.textContent = "";
els.instrFilterSection.hidden = true;
els.detailPane.classList.add("collapsed");
els.emptyOverlay.classList.remove("hidden");
els.selfLoopPaths.innerHTML = "";
if (cy) { cy.destroy(); cy = null; }
return;
}
const t1 = performance.now();
setStatus("Rendering…");
requestAnimationFrame(() => {
renderBlockList();
renderInstrFilter();
renderGraph();
const t2 = performance.now();
const summary =
blocks.length + " blocks · " +
countEdges() + " edges · parsed " + ((t1 - t0) | 0) + " ms · layout " + ((t2 - t1) | 0) + " ms";
setStatus(summary);
els.detailPane.classList.add("collapsed");
selectedId = null;
});
});
}
function countEdges() {
let n = 0;
for (const b of blocks) for (const s of b.successors) if (byLabel.has(s)) n++;
return n;
}
// -------------------- Event wiring --------------------
els.fileStir.addEventListener("change", ev => {
const f = ev.target.files && ev.target.files[0];
if (!f) return;
loadFile(f);
// reset the input so re-loading the same file fires `change` again
ev.target.value = "";
});
function loadFile(file) {
setStatus("Reading " + file.name + " (" + formatBytes(file.size) + ")…");
const r = new FileReader();
r.onload = () => {
const text = String(r.result || "");
els.stirEditor.value = text;
updateStirEditorHighlight();
ingestText(text, file.name);
};
r.onerror = () => setStatus("Read error: " + (r.error && r.error.message || "unknown"));
r.readAsText(file);
}
function formatBytes(n) {
if (n < 1024) return n + " B";
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + " KB";
return (n / (1024 * 1024)).toFixed(2) + " MB";
}
els.btnLoadEditor.addEventListener("click", () => {
const text = els.stirEditor.value || "";
if (!text.trim()) {
setStatus("STIR editor is empty");
return;
}
ingestText(text, "editor");
});
els.btnClearEditor.addEventListener("click", () => {
els.stirEditor.value = "";
updateStirEditorHighlight();
els.stirEditor.focus();
setStatus("STIR editor cleared");
});
els.stirEditor.addEventListener("input", updateStirEditorHighlight);
els.stirEditor.addEventListener("scroll", syncStirEditorScroll);
els.btnFit.addEventListener("click", () => { if (cy) cy.animate({ fit: { padding: 30 }, duration: 240 }); });
els.btnReset.addEventListener("click", () => { if (cy) cy.layout(layoutOpts()).run(); });
els.btnToggleEditor.addEventListener("click", () => {
const collapsed = els.editorPanel.classList.toggle("collapsed");
els.btnToggleEditor.textContent = collapsed ? "Show editor" : "Hide editor";
els.btnToggleEditor.setAttribute("aria-expanded", collapsed ? "false" : "true");
requestAnimationFrame(() => {
if (cy) {
cy.resize();
syncSelfLoopOverlay();
}
});
});
els.editorResizeHandle.addEventListener("pointerdown", ev => {
ev.preventDefault();
const startY = ev.clientY;
const startHeight = els.editorPanel.getBoundingClientRect().height;
const minHeight = 128;
const maxHeight = Math.max(minHeight, window.innerHeight * 0.45);
els.editorPanel.classList.add("resizing");
document.body.classList.add("resizing-editor");
els.editorResizeHandle.setPointerCapture(ev.pointerId);
function onPointerMove(moveEv) {
const nextHeight = Math.max(minHeight, Math.min(maxHeight, startHeight - (moveEv.clientY - startY)));
els.editorPanel.style.height = nextHeight + "px";
if (cy) {
cy.resize();
syncSelfLoopOverlay();
}
}
function onPointerUp(upEv) {
els.editorPanel.classList.remove("resizing");
document.body.classList.remove("resizing-editor");
els.editorResizeHandle.releasePointerCapture(upEv.pointerId);
els.editorResizeHandle.removeEventListener("pointermove", onPointerMove);
els.editorResizeHandle.removeEventListener("pointerup", onPointerUp);
els.editorResizeHandle.removeEventListener("pointercancel", onPointerUp);
}
els.editorResizeHandle.addEventListener("pointermove", onPointerMove);
els.editorResizeHandle.addEventListener("pointerup", onPointerUp);
els.editorResizeHandle.addEventListener("pointercancel", onPointerUp);
});
els.showInstrInNodes.addEventListener("change", () => {
showInstructionsInNodes = els.showInstrInNodes.checked;
refreshNodeLabels(true);
});
els.layoutSelect.addEventListener("change", () => { if (cy) cy.layout(layoutOpts()).run(); });
els.sidebarToggle.addEventListener("click", () => {
document.querySelector(".sidebar").classList.toggle("collapsed");
});
if (window.ResizeObserver) {
const graphResizeObserver = new ResizeObserver(() => {
if (!cy) return;
cy.resize();
syncSelfLoopOverlay();
});
graphResizeObserver.observe(els.graphPane);
const sidebarResizeObserver = new ResizeObserver(enforceCompactSidebarLists);
sidebarResizeObserver.observe(document.querySelector(".sidebar"));
}
els.blockFilter.addEventListener("input", applyBlockFilter);
els.instrFilterToggle.addEventListener("click", () => toggleLargeSidebarSection("instr"));
els.blockListToggle.addEventListener("click", () => toggleLargeSidebarSection("blocks"));
els.instrTypeFilter.addEventListener("input", applyInstrTypeSearch);
els.instrSelectAll.addEventListener("click", () => setVisibleTypes(true));
els.instrClear.addEventListener("click", clearTypeSelection);
els.labelSearch.addEventListener("keydown", ev => {
if (ev.key === "Enter") { ev.preventDefault(); jumpToSearch(); }
});
els.labelSearch.addEventListener("change", () => {
// Triggered by datalist selection in most browsers.
if (els.labelSearch.value) jumpToSearch();
});
els.dhClose.addEventListener("click", () => {
els.detailPane.classList.add("collapsed");
clearGraphHighlight();
markBlockListSelection(null);
selectedId = null;
});
els.copyBtn.addEventListener("click", copyInstructions);
// -------------------- Drag & drop --------------------
let dragDepth = 0;
function onDragEnter(e) {
if (!hasFiles(e)) return;
dragDepth++;
els.dropOverlay.classList.add("visible");
e.preventDefault();
}
function onDragOver(e) {
if (!hasFiles(e)) return;
e.preventDefault();
if (e.dataTransfer) e.dataTransfer.dropEffect = "copy";
}
function onDragLeave(e) {
if (!hasFiles(e)) return;
dragDepth = Math.max(0, dragDepth - 1);
if (dragDepth === 0) els.dropOverlay.classList.remove("visible");
}
function onDrop(e) {
e.preventDefault();
dragDepth = 0;
els.dropOverlay.classList.remove("visible");
const file = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0];
if (file) loadFile(file);
}
function hasFiles(e) {
const dt = e.dataTransfer;
if (!dt) return false;
if (dt.types) {
for (let i = 0; i < dt.types.length; i++) if (dt.types[i] === "Files") return true;
}
return false;
}
window.addEventListener("dragenter", onDragEnter);
window.addEventListener("dragover", onDragOver);
window.addEventListener("dragleave", onDragLeave);
window.addEventListener("drop", onDrop);
// -------------------- Keyboard shortcuts --------------------
document.addEventListener("keydown", ev => {
// Don't intercept while typing in inputs.
const tag = (ev.target && ev.target.tagName) || "";
const inField = tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT";
if (ev.key === "/" && !inField) {
ev.preventDefault();
els.labelSearch.focus();
els.labelSearch.select();
} else if (ev.key === "Escape" && !els.detailPane.classList.contains("collapsed")) {
els.detailPane.classList.add("collapsed");
clearGraphHighlight();
markBlockListSelection(null);
selectedId = null;
} else if (ev.key === "f" && !inField) {
if (cy) cy.animate({ fit: { padding: 30 }, duration: 240 });
}
});
setStatus("Ready · drop a .stir file or paste STIR into the editor");
})();
</script>
</body>
</html>