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
2237 lines
68 KiB
HTML
2237 lines
68 KiB
HTML
<!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 & 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, "&").replace(/</g, "<").replace(/>/g, ">");
|
||
}
|
||
|
||
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>
|