mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2025-12-27 10:33:59 -05:00
Control flow graph tool rework (#3899)
* Removed old code * Base functionality * Work on edge offsets * Setup interval trees for edges * linter things * Formatting * Added syntax highlighting * Cleanup and simplification. Improved handling of direct dropdown edges. * Basic zoom/pan * Remove old blocks from output * Fix distance calculation * Added function selector * Improved zoom behavior * figue 8 logic * Canvas scaling / repainting, updated colors * Don't truncate output, also removed some console.logs * Tweak to zoom system * Replaced canvas stuff with svg * Experimenting with adding shadows to edges * Removed shadows, was causing problems. Improved how blocks with lots of incident edges are handled. * Slightly improved edge system * some work on implementing segment priority system from cutter * Optimization to rendering process. I was worried the graph layout algorithm was causing the page to hang but it turns out it was adding elements to the page with += * Removed need for storing the previous segment * refactor, splitting up some logic * Cleaned up logic and got horizontal edges working better * Remove vis-network dependency * Updated package-lock, removed @import vis-network css stuff, added a todo for myself * Cleaned up notes and error messages. Added comments. Clear the pane if there's no function to display * Added layout time information, implemented .resize * Light theme * State work and bug fix for dragiing * Re-dading lost dark theme changes * Added jquery import * Cleaned up console.logs * Added basic block count * Incorporated PR review comments; Fixed cypress (hopefully), added documentation, improved the dropdown, and fixed dropdown items not being cleared with an empty result.cfg * Ran format
This commit is contained in:
@@ -18,7 +18,7 @@ const PANE_DATA_MAP = {
|
||||
dump: {name: 'Tree/RTL', selector: 'view-gccdump'},
|
||||
tree: {name: 'Tree', selector: 'view-gnatdebugtree'},
|
||||
debug: {name: 'Debug', selector: 'view-gnatdebug'},
|
||||
cfg: {name: 'Graph', selector: 'view-cfg'},
|
||||
cfg: {name: 'CFG', selector: 'view-cfg'},
|
||||
};
|
||||
|
||||
describe('Individual pane testing', () => {
|
||||
|
||||
@@ -217,7 +217,7 @@ function splitToCanonicalBasicBlock(basicBlock) {
|
||||
|
||||
function concatInstructions(asmArr, first, last) {
|
||||
return _.chain(asmArr.slice(first, last))
|
||||
.map(x => x.text.substr(0, 50))
|
||||
.map(x => x.text)
|
||||
.value()
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
150
package-lock.json
generated
150
package-lock.json
generated
@@ -9,6 +9,7 @@
|
||||
"version": "0.0.3",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@flatten-js/interval-tree": "^1.0.18",
|
||||
"@fortawesome/fontawesome-free": "^5.15.4",
|
||||
"@sentry/browser": "^6.16.1",
|
||||
"@sentry/node": "^6.16.1",
|
||||
@@ -71,7 +72,6 @@
|
||||
"tslib": "^2.3.1",
|
||||
"underscore": "^1.13.2",
|
||||
"url-join": "^4.0.1",
|
||||
"vis-network": "^9.1.0",
|
||||
"whatwg-fetch": "^3.6.2",
|
||||
"which": "^2.0.2",
|
||||
"winston": "^3.3.3",
|
||||
@@ -701,18 +701,6 @@
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@egjs/hammerjs": {
|
||||
"version": "2.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz",
|
||||
"integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/hammerjs": "^2.0.36"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@es-joy/jsdoccomment": {
|
||||
"version": "0.20.1",
|
||||
"resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.20.1.tgz",
|
||||
@@ -791,6 +779,11 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@flatten-js/interval-tree": {
|
||||
"version": "1.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@flatten-js/interval-tree/-/interval-tree-1.0.18.tgz",
|
||||
"integrity": "sha512-o72sZErW0Y1C82Cg7nk82ojJ/22EtmKyp5I3eNqgcOKFp/VCzetATYYjJIqOBBaR7FQ/MFj/ZpsmP38mL4TkYA=="
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-free": {
|
||||
"version": "5.15.4",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz",
|
||||
@@ -1926,12 +1919,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/hammerjs": {
|
||||
"version": "2.0.41",
|
||||
"resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.41.tgz",
|
||||
"integrity": "sha512-ewXv/ceBaJprikMcxCmWU1FKyMAQ2X7a9Gtmzw8fcg2kIePI1crERDM818W+XYrxqdBBOdlf2rm137bU+BltCA==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/http-proxy": {
|
||||
"version": "1.17.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.9.tgz",
|
||||
@@ -4014,7 +4001,8 @@
|
||||
"node_modules/component-emitter": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
|
||||
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
|
||||
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/compressible": {
|
||||
"version": "2.0.18",
|
||||
@@ -8415,12 +8403,6 @@
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/keycharm": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/keycharm/-/keycharm-0.4.0.tgz",
|
||||
"integrity": "sha512-TyQTtsabOVv3MeOpR92sIKk/br9wxS+zGj4BG7CR8YbK4jM3tyIBaF0zhzeBUMx36/Q/iQLOKKOT+3jOQtemRQ==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/kind-of": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||
@@ -13785,12 +13767,6 @@
|
||||
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/timsort": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
|
||||
"integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tiny-emitter": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
|
||||
@@ -14357,6 +14333,7 @@
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
@@ -14407,57 +14384,6 @@
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="
|
||||
},
|
||||
"node_modules/vis-data": {
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/vis-data/-/vis-data-7.1.4.tgz",
|
||||
"integrity": "sha512-usy+ePX1XnArNvJ5BavQod7YRuGQE1pjFl+pu7IS6rCom2EBoG0o1ZzCqf3l5US6MW51kYkLR+efxRbnjxNl7w==",
|
||||
"hasInstallScript": true,
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/visjs"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"uuid": "^7.0.0 || ^8.0.0",
|
||||
"vis-util": "^4.0.0 || ^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vis-network": {
|
||||
"version": "9.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vis-network/-/vis-network-9.1.2.tgz",
|
||||
"integrity": "sha512-BdapguKg7sk3NvdZaDsM7T6rNhOBFz0/F4ZScxctK4klRzQPLQPTEcmbioXaZhMkkgWymzBR3lFCxL1q+eYyAw==",
|
||||
"hasInstallScript": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/visjs"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@egjs/hammerjs": "^2.0.0",
|
||||
"component-emitter": "^1.3.0",
|
||||
"keycharm": "^0.2.0 || ^0.3.0 || ^0.4.0",
|
||||
"timsort": "^0.3.0",
|
||||
"uuid": "^3.4.0 || ^7.0.0 || ^8.0.0",
|
||||
"vis-data": "^7.0.0",
|
||||
"vis-util": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/vis-util": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/vis-util/-/vis-util-5.0.3.tgz",
|
||||
"integrity": "sha512-Wf9STUcFrDzK4/Zr7B6epW2Kvm3ORNWF+WiwEz2dpf5RdWkLUXFSbLcuB88n1W6tCdFwVN+v3V4/Xmn9PeL39g==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/visjs"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@egjs/hammerjs": "^2.0.0",
|
||||
"component-emitter": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/void-elements": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||
@@ -15809,15 +15735,6 @@
|
||||
"integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==",
|
||||
"dev": true
|
||||
},
|
||||
"@egjs/hammerjs": {
|
||||
"version": "2.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz",
|
||||
"integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==",
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"@types/hammerjs": "^2.0.36"
|
||||
}
|
||||
},
|
||||
"@es-joy/jsdoccomment": {
|
||||
"version": "0.20.1",
|
||||
"resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.20.1.tgz",
|
||||
@@ -15876,6 +15793,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@flatten-js/interval-tree": {
|
||||
"version": "1.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@flatten-js/interval-tree/-/interval-tree-1.0.18.tgz",
|
||||
"integrity": "sha512-o72sZErW0Y1C82Cg7nk82ojJ/22EtmKyp5I3eNqgcOKFp/VCzetATYYjJIqOBBaR7FQ/MFj/ZpsmP38mL4TkYA=="
|
||||
},
|
||||
"@fortawesome/fontawesome-free": {
|
||||
"version": "5.15.4",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz",
|
||||
@@ -16787,12 +16709,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/hammerjs": {
|
||||
"version": "2.0.41",
|
||||
"resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.41.tgz",
|
||||
"integrity": "sha512-ewXv/ceBaJprikMcxCmWU1FKyMAQ2X7a9Gtmzw8fcg2kIePI1crERDM818W+XYrxqdBBOdlf2rm137bU+BltCA==",
|
||||
"peer": true
|
||||
},
|
||||
"@types/http-proxy": {
|
||||
"version": "1.17.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.9.tgz",
|
||||
@@ -18395,7 +18311,8 @@
|
||||
"component-emitter": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
|
||||
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
|
||||
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==",
|
||||
"dev": true
|
||||
},
|
||||
"compressible": {
|
||||
"version": "2.0.18",
|
||||
@@ -21621,12 +21538,6 @@
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"keycharm": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/keycharm/-/keycharm-0.4.0.tgz",
|
||||
"integrity": "sha512-TyQTtsabOVv3MeOpR92sIKk/br9wxS+zGj4BG7CR8YbK4jM3tyIBaF0zhzeBUMx36/Q/iQLOKKOT+3jOQtemRQ==",
|
||||
"peer": true
|
||||
},
|
||||
"kind-of": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||
@@ -25627,12 +25538,6 @@
|
||||
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
|
||||
"dev": true
|
||||
},
|
||||
"timsort": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
|
||||
"integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==",
|
||||
"peer": true
|
||||
},
|
||||
"tiny-emitter": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
|
||||
@@ -26046,7 +25951,8 @@
|
||||
"uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"dev": true
|
||||
},
|
||||
"v8-compile-cache": {
|
||||
"version": "2.3.0",
|
||||
@@ -26090,26 +25996,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"vis-data": {
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/vis-data/-/vis-data-7.1.4.tgz",
|
||||
"integrity": "sha512-usy+ePX1XnArNvJ5BavQod7YRuGQE1pjFl+pu7IS6rCom2EBoG0o1ZzCqf3l5US6MW51kYkLR+efxRbnjxNl7w==",
|
||||
"peer": true,
|
||||
"requires": {}
|
||||
},
|
||||
"vis-network": {
|
||||
"version": "9.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vis-network/-/vis-network-9.1.2.tgz",
|
||||
"integrity": "sha512-BdapguKg7sk3NvdZaDsM7T6rNhOBFz0/F4ZScxctK4klRzQPLQPTEcmbioXaZhMkkgWymzBR3lFCxL1q+eYyAw==",
|
||||
"requires": {}
|
||||
},
|
||||
"vis-util": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/vis-util/-/vis-util-5.0.3.tgz",
|
||||
"integrity": "sha512-Wf9STUcFrDzK4/Zr7B6epW2Kvm3ORNWF+WiwEz2dpf5RdWkLUXFSbLcuB88n1W6tCdFwVN+v3V4/Xmn9PeL39g==",
|
||||
"peer": true,
|
||||
"requires": {}
|
||||
},
|
||||
"void-elements": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"cache": false
|
||||
},
|
||||
"dependencies": {
|
||||
"@flatten-js/interval-tree": "^1.0.18",
|
||||
"@fortawesome/fontawesome-free": "^5.15.4",
|
||||
"@sentry/browser": "^6.16.1",
|
||||
"@sentry/node": "^6.16.1",
|
||||
@@ -82,7 +83,6 @@
|
||||
"tslib": "^2.3.1",
|
||||
"underscore": "^1.13.2",
|
||||
"url-join": "^4.0.1",
|
||||
"vis-network": "^9.1.0",
|
||||
"whatwg-fetch": "^3.6.2",
|
||||
"which": "^2.0.2",
|
||||
"winston": "^3.3.3",
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
// POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import {CompilerOutputOptions} from '../types/features/filters.interfaces';
|
||||
import {CfgState} from './panes/cfg-view.interfaces';
|
||||
import {LLVMOptPipelineViewState} from './panes/llvm-opt-pipeline.interfaces';
|
||||
export const COMPILER_COMPONENT_NAME = 'compiler';
|
||||
export const EXECUTOR_COMPONENT_NAME = 'executor';
|
||||
@@ -175,10 +176,11 @@ export type PopulatedGccDumpViewState = {
|
||||
} & (Record<GccDumpOptions, unknown> | EmptyState);
|
||||
|
||||
export type EmptyCfgViewState = EmptyState;
|
||||
export type PopulatedCfgViewState = StateWithId & {
|
||||
editorid: number;
|
||||
treeid: number;
|
||||
};
|
||||
export type PopulatedCfgViewState = StateWithId &
|
||||
CfgState & {
|
||||
editorid: number;
|
||||
treeid: number;
|
||||
};
|
||||
|
||||
export type EmptyConformanceViewState = EmptyState; // TODO: unusued?
|
||||
export type PopulatedConformanceViewState = {
|
||||
|
||||
@@ -526,6 +526,8 @@ export function getCfgViewWith(id: number, editorid: number, treeid: number): Co
|
||||
type: 'component',
|
||||
componentName: CFG_VIEW_COMPONENT_NAME,
|
||||
componentState: {
|
||||
selectedFunction: null,
|
||||
zoom: 1,
|
||||
id,
|
||||
editorid,
|
||||
treeid,
|
||||
|
||||
951
static/graph-layout-core.ts
Normal file
951
static/graph-layout-core.ts
Normal file
@@ -0,0 +1,951 @@
|
||||
// Copyright (c) 2022, Compiler Explorer Authors
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// * Redistributions of source code must retain the above copyright notice,
|
||||
// this list of conditions and the following disclaimer.
|
||||
// * Redistributions in binary form must reproduce the above copyright
|
||||
// notice, this list of conditions and the following disclaimer in the
|
||||
// documentation and/or other materials provided with the distribution.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
// POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import {AnnotatedCfgDescriptor, AnnotatedNodeDescriptor} from '../types/compilation/cfg.interfaces';
|
||||
|
||||
import IntervalTree, {Node} from '@flatten-js/interval-tree';
|
||||
|
||||
// Much of the algorithm is inspired from
|
||||
// https://cutter.re/docs/api/widgets/classGraphGridLayout.html
|
||||
// Thanks to the cutter team for their great documentation!
|
||||
|
||||
// TODO(jeremy-rifkin)
|
||||
function assert(condition: boolean, message?: string, ...args: any[]): asserts condition {
|
||||
if (!condition) {
|
||||
const stack = new Error('Assertion Error').stack;
|
||||
throw (
|
||||
(message
|
||||
? `Assertion error in llvm-print-after-all-parser: ${message}`
|
||||
: `Assertion error in llvm-print-after-all-parser`) +
|
||||
(args.length > 0 ? `\n${JSON.stringify(args)}\n` : '') +
|
||||
`\n${stack}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum SegmentType {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
type Coordinate = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
type GridCoordinate = {
|
||||
row: number;
|
||||
col: number;
|
||||
};
|
||||
|
||||
type EdgeCoordinate = Coordinate & GridCoordinate;
|
||||
|
||||
type EdgeSegment = {
|
||||
start: EdgeCoordinate;
|
||||
end: EdgeCoordinate;
|
||||
horizontalOffset: number;
|
||||
verticalOffset: number;
|
||||
type: SegmentType; // is this point the end of a horizontal or vertical segment
|
||||
};
|
||||
|
||||
type Edge = {
|
||||
color: string;
|
||||
dest: number;
|
||||
mainColumn: number;
|
||||
path: EdgeSegment[];
|
||||
};
|
||||
|
||||
type BoundingBox = {
|
||||
rows: number;
|
||||
cols: number;
|
||||
};
|
||||
|
||||
type Block = {
|
||||
data: AnnotatedNodeDescriptor;
|
||||
edges: Edge[];
|
||||
dagEdges: number[];
|
||||
treeEdges: number[];
|
||||
treeParent: number | null;
|
||||
row: number;
|
||||
col: number;
|
||||
boundingBox: BoundingBox;
|
||||
coordinates: Coordinate;
|
||||
incidentEdgeCount: number;
|
||||
};
|
||||
|
||||
enum DfsState {
|
||||
NotVisited,
|
||||
Pending,
|
||||
Visited,
|
||||
}
|
||||
|
||||
type ColumnDescriptor = {
|
||||
width: number;
|
||||
totalOffset: number;
|
||||
};
|
||||
type RowDescriptor = {
|
||||
height: number;
|
||||
totalOffset: number;
|
||||
};
|
||||
type EdgeColumnMetadata = {
|
||||
subcolumns: number;
|
||||
intervals: IntervalTree<EdgeSegment>[]; // pointers to segments
|
||||
};
|
||||
type EdgeRowMetadata = {
|
||||
subrows: number;
|
||||
intervals: IntervalTree<EdgeSegment>[]; // pointers to segments
|
||||
};
|
||||
|
||||
const EDGE_SPACING = 10;
|
||||
|
||||
export class GraphLayoutCore {
|
||||
// We use an adjacency list here
|
||||
blocks: Block[] = [];
|
||||
columnCount: number;
|
||||
rowCount: number;
|
||||
blockColumns: ColumnDescriptor[];
|
||||
blockRows: RowDescriptor[];
|
||||
edgeColumns: (ColumnDescriptor & EdgeColumnMetadata)[];
|
||||
edgeRows: (RowDescriptor & EdgeRowMetadata)[];
|
||||
readonly layoutTime: number;
|
||||
|
||||
constructor(cfg: AnnotatedCfgDescriptor) {
|
||||
// block id -> block
|
||||
const blockMap: Record<string, number> = {};
|
||||
for (const node of cfg.nodes) {
|
||||
const block = {
|
||||
data: node,
|
||||
edges: [],
|
||||
dagEdges: [],
|
||||
treeEdges: [],
|
||||
treeParent: null,
|
||||
row: 0,
|
||||
col: 0,
|
||||
boundingBox: {rows: 0, cols: 0},
|
||||
coordinates: {x: 0, y: 0},
|
||||
incidentEdgeCount: 0,
|
||||
};
|
||||
this.blocks.push(block);
|
||||
blockMap[node.id] = this.blocks.length - 1;
|
||||
}
|
||||
for (const {from, to, color} of cfg.edges) {
|
||||
// TODO: Backend can return dest: "null"
|
||||
// e.g. for the simple program
|
||||
// void baz(int n) {
|
||||
// if(n % 2 == 0) {
|
||||
// foo();
|
||||
// } else {
|
||||
// bar();
|
||||
// }
|
||||
// }
|
||||
if (from in blockMap && to in blockMap) {
|
||||
this.blocks[blockMap[from]].edges.push({
|
||||
color,
|
||||
dest: blockMap[to],
|
||||
mainColumn: -1,
|
||||
path: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
//console.log(this.blocks);
|
||||
const start = performance.now();
|
||||
this.layout();
|
||||
const end = performance.now();
|
||||
this.layoutTime = end - start;
|
||||
}
|
||||
|
||||
dfs(visited: DfsState[], order: number[], node: number) {
|
||||
if (visited[node] === DfsState.Visited) {
|
||||
return;
|
||||
}
|
||||
if (visited[node] === DfsState.NotVisited) {
|
||||
visited[node] = DfsState.Pending;
|
||||
const block = this.blocks[node];
|
||||
for (const edge of block.edges) {
|
||||
this.blocks[edge.dest].incidentEdgeCount++;
|
||||
// If we reach another pending node it's a loop edge.
|
||||
// If we reach an unvisited node it's fine, if we reach a visited node that's also part of the dag
|
||||
if (visited[edge.dest] !== DfsState.Pending) {
|
||||
block.dagEdges.push(edge.dest);
|
||||
}
|
||||
this.dfs(visited, order, edge.dest);
|
||||
}
|
||||
visited[node] = DfsState.Visited;
|
||||
order.push(node);
|
||||
} else {
|
||||
// visited[node] == DfsState.Pending
|
||||
// If we reach a node in the stack then this is a loop edge; we do nothing
|
||||
}
|
||||
}
|
||||
|
||||
computeDag() {
|
||||
// Returns a topological order of blocks
|
||||
// Breaks loop edges with DFS
|
||||
// Can consider doing non-recursive dfs later if needed
|
||||
const visited = Array(this.blocks.length).fill(DfsState.NotVisited);
|
||||
const order: number[] = [];
|
||||
// TODO: Need an actual function entry point from the backend, or will it always be at index 0?
|
||||
this.dfs(visited, order, 0);
|
||||
for (let i = 0; i < this.blocks.length; i++) {
|
||||
this.dfs(visited, order, i);
|
||||
}
|
||||
// we've computed a post-DFS ordering which is always a reverse topological ordering
|
||||
return order.reverse();
|
||||
}
|
||||
|
||||
assignRows(topologicalOrder) {
|
||||
for (const i of topologicalOrder) {
|
||||
const block = this.blocks[i];
|
||||
//console.log(block);
|
||||
for (const j of block.dagEdges) {
|
||||
const target = this.blocks[j];
|
||||
target.row = Math.max(target.row, block.row + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computeTree(topologicalOrder) {
|
||||
// DAG is reduced to a tree based on what's vertically adjacent
|
||||
//
|
||||
// For something like
|
||||
//
|
||||
// +-----+
|
||||
// | A |
|
||||
// +-----+
|
||||
// / \
|
||||
// +-----+ +-----+
|
||||
// | B | | C |
|
||||
// +-----+ +-----+
|
||||
// \ /
|
||||
// +-----+
|
||||
// | D |
|
||||
// +-----+
|
||||
//
|
||||
// The tree is chosen to be either of the following depending on what the topological order happens to be
|
||||
// This doesn't matter too much as far as readability goes
|
||||
//
|
||||
// A A
|
||||
// / \ / \
|
||||
// B C or B C
|
||||
// | |
|
||||
// D D
|
||||
for (const i of topologicalOrder) {
|
||||
// Only dag edges are considered
|
||||
// Edges - dag edges = the set of back edges
|
||||
const block = this.blocks[i];
|
||||
for (const j of block.dagEdges) {
|
||||
const target = this.blocks[j];
|
||||
if (target.treeParent === null && target.row === block.row + 1) {
|
||||
block.treeEdges.push(j);
|
||||
target.treeParent = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
adjustSubtree(root: number, rowShift: number, columnShift: number) {
|
||||
const block = this.blocks[root];
|
||||
block.row += rowShift;
|
||||
block.col += columnShift;
|
||||
for (const j of block.treeEdges) {
|
||||
this.adjustSubtree(j, rowShift, columnShift);
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Currently O(n^2)
|
||||
computeTreeColumnPositions(node: number) {
|
||||
const block = this.blocks[node];
|
||||
if (block.treeEdges.length === 0) {
|
||||
block.row = 0;
|
||||
block.col = 0;
|
||||
block.boundingBox = {
|
||||
rows: 1,
|
||||
cols: 2,
|
||||
};
|
||||
} else if (block.treeEdges.length === 1) {
|
||||
const childIndex = block.treeEdges[0];
|
||||
const child = this.blocks[childIndex];
|
||||
block.row = 0;
|
||||
block.col = child.col;
|
||||
block.boundingBox = {
|
||||
rows: 1 + child.boundingBox.rows,
|
||||
cols: child.boundingBox.cols,
|
||||
};
|
||||
this.adjustSubtree(childIndex, 1, 0);
|
||||
} else {
|
||||
// If the node has more than two children we'll just center between the
|
||||
//let selectedTreeEdges = block.treeEdges.slice(0, 2);
|
||||
const boundingBox = {
|
||||
rows: 0,
|
||||
cols: 0,
|
||||
};
|
||||
// Compute bounding box of all the subtrees and adjust
|
||||
for (const i of block.treeEdges) {
|
||||
const child = this.blocks[i];
|
||||
this.adjustSubtree(i, 1, boundingBox.cols);
|
||||
boundingBox.rows += child.boundingBox.rows;
|
||||
boundingBox.cols += child.boundingBox.cols;
|
||||
}
|
||||
// Position parent
|
||||
boundingBox.rows++;
|
||||
block.boundingBox = boundingBox;
|
||||
block.row = 0;
|
||||
// between immediate children
|
||||
const [left, right] = [this.blocks[block.treeEdges[0]], this.blocks[block.treeEdges[1]]];
|
||||
block.col = Math.floor((left.col + right.col) / 2); // TODO
|
||||
}
|
||||
}
|
||||
|
||||
assignColumns(topologicalOrder) {
|
||||
// Note: Currently not taking shape into account like Cutter does.
|
||||
// Post DFS order means we compute all children before their parents
|
||||
for (const i of topologicalOrder.slice().reverse()) {
|
||||
this.computeTreeColumnPositions(i);
|
||||
}
|
||||
// We have a forrest, CFGs can have multiple source nodes
|
||||
const trees = Array.from(this.blocks.entries()).filter(([_, block]) => block.treeParent === null);
|
||||
// Place trees next to each other
|
||||
let offset = 0;
|
||||
for (const [i, tree] of trees) {
|
||||
this.adjustSubtree(i, 0, offset);
|
||||
offset += tree.boundingBox.cols;
|
||||
}
|
||||
}
|
||||
|
||||
setupRowsAndColumns() {
|
||||
//console.log(this.blocks);
|
||||
this.rowCount = Math.max(...this.blocks.map(block => block.row)) + 1; // one more row for zero offset
|
||||
this.columnCount = Math.max(...this.blocks.map(block => block.col)) + 2; // blocks are two-wide
|
||||
this.blockRows = Array(this.rowCount)
|
||||
.fill(0)
|
||||
.map(() => ({
|
||||
height: 0,
|
||||
totalOffset: 0,
|
||||
}));
|
||||
this.blockColumns = Array(this.columnCount)
|
||||
.fill(0)
|
||||
.map(() => ({
|
||||
width: 0,
|
||||
totalOffset: 0,
|
||||
}));
|
||||
this.edgeRows = Array(this.rowCount + 1)
|
||||
.fill(0)
|
||||
.map(() => ({
|
||||
height: 2 * EDGE_SPACING,
|
||||
totalOffset: 0,
|
||||
subrows: 0,
|
||||
intervals: [],
|
||||
}));
|
||||
this.edgeColumns = Array(this.columnCount + 1)
|
||||
.fill(0)
|
||||
.map(() => ({
|
||||
width: 2 * EDGE_SPACING,
|
||||
totalOffset: 0,
|
||||
subcolumns: 0,
|
||||
intervals: [],
|
||||
}));
|
||||
}
|
||||
|
||||
computeEdgeMainColumns() {
|
||||
// This is heavily inspired by Cutter
|
||||
// We use a sweep line algorithm processing the CFG top to bottom keeping track of when columns are most
|
||||
// recently blocked. Cutter uses an augmented binary tree to assist with finding empty columns, for now this
|
||||
// just naively iterates.
|
||||
enum EventType {
|
||||
Edge = 0,
|
||||
Block = 1,
|
||||
}
|
||||
type Event = {
|
||||
blockIndex: number;
|
||||
edgeIndex: number;
|
||||
row: number;
|
||||
type: EventType;
|
||||
};
|
||||
const events: Event[] = [];
|
||||
for (const [i, block] of this.blocks.entries()) {
|
||||
events.push({
|
||||
blockIndex: i,
|
||||
edgeIndex: -1,
|
||||
row: block.row,
|
||||
type: EventType.Block,
|
||||
});
|
||||
for (const [j, edge] of block.edges.entries()) {
|
||||
events.push({
|
||||
blockIndex: i,
|
||||
edgeIndex: j,
|
||||
row: Math.max(block.row + 1, this.blocks[edge.dest].row),
|
||||
type: EventType.Edge,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Sort by row (max(src row, target row) for edges), edge row n is before block row n
|
||||
events.sort((a: Event, b: Event) => {
|
||||
if (a.row === b.row) {
|
||||
return a.type - b.type;
|
||||
} else {
|
||||
return a.row - b.row;
|
||||
}
|
||||
});
|
||||
//
|
||||
const blockedColumns = Array(this.columnCount + 1).fill(-1);
|
||||
for (const event of events) {
|
||||
if (event.type === EventType.Block) {
|
||||
const block = this.blocks[event.blockIndex];
|
||||
blockedColumns[block.col + 1] = block.row;
|
||||
} else {
|
||||
const source = this.blocks[event.blockIndex];
|
||||
const edge = source.edges[event.edgeIndex];
|
||||
const target = this.blocks[edge.dest];
|
||||
const sourceColumn = source.col + 1;
|
||||
const targetColumn = target.col + 1;
|
||||
const topRow = Math.min(source.row + 1, target.row);
|
||||
if (blockedColumns[sourceColumn] < topRow) {
|
||||
// use column under source block
|
||||
edge.mainColumn = sourceColumn;
|
||||
} else if (blockedColumns[targetColumn] < topRow) {
|
||||
// use column of the target
|
||||
edge.mainColumn = targetColumn;
|
||||
} else {
|
||||
const leftCandidate =
|
||||
sourceColumn -
|
||||
1 -
|
||||
blockedColumns
|
||||
.slice(0, sourceColumn)
|
||||
.reverse()
|
||||
.findIndex(v => v < topRow);
|
||||
const rightCandidate = sourceColumn + blockedColumns.slice(sourceColumn).findIndex(v => v < topRow);
|
||||
// hamming distance
|
||||
const distanceLeft =
|
||||
Math.abs(sourceColumn - leftCandidate) + Math.abs(targetColumn - leftCandidate);
|
||||
const distanceRight =
|
||||
Math.abs(sourceColumn - rightCandidate) + Math.abs(targetColumn - rightCandidate);
|
||||
// "figure 8" logic from cutter
|
||||
// Takes a longer path that produces less crossing
|
||||
if (target.row < source.row) {
|
||||
if (
|
||||
targetColumn < sourceColumn &&
|
||||
blockedColumns[sourceColumn + 1] < topRow &&
|
||||
sourceColumn - targetColumn <= distanceLeft + 2
|
||||
) {
|
||||
edge.mainColumn = sourceColumn + 1;
|
||||
continue;
|
||||
} else if (
|
||||
targetColumn > sourceColumn &&
|
||||
blockedColumns[sourceColumn - 1] < topRow &&
|
||||
targetColumn - sourceColumn <= distanceRight + 2
|
||||
) {
|
||||
edge.mainColumn = sourceColumn - 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (distanceLeft === distanceRight) {
|
||||
// TODO: Could also try this
|
||||
/*if(target.row <= source.row) {
|
||||
if(leftCandidate === sourceColumn - 1) {
|
||||
edge.mainColumn = leftCandidate;
|
||||
continue;
|
||||
} else if(rightCandidate === sourceColumn + 1) {
|
||||
edge.mainColumn = rightCandidate;
|
||||
continue;
|
||||
}
|
||||
}*/
|
||||
// Place true branches on the left
|
||||
// TODO: Need to investigate further block placement stuff here
|
||||
// TODO: Need to investigate further offset placement stuff for the start segments
|
||||
if (edge.color === 'green') {
|
||||
edge.mainColumn = leftCandidate;
|
||||
} else {
|
||||
edge.mainColumn = rightCandidate;
|
||||
}
|
||||
} else if (distanceLeft < distanceRight) {
|
||||
edge.mainColumn = leftCandidate;
|
||||
} else {
|
||||
edge.mainColumn = rightCandidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-statements
|
||||
addEdgePaths() {
|
||||
// (start: GridCoordinate, end: GridCoordinate) => ({
|
||||
const makeSegment = (start: [number, number], end: [number, number]): EdgeSegment => ({
|
||||
start: {
|
||||
//...start,
|
||||
row: start[0],
|
||||
col: start[1],
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
end: {
|
||||
//...end,
|
||||
row: end[0],
|
||||
col: end[1],
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
horizontalOffset: 0,
|
||||
verticalOffset: 0,
|
||||
type: start[1] === end[1] ? SegmentType.Vertical : SegmentType.Horizontal,
|
||||
});
|
||||
for (const block of this.blocks) {
|
||||
for (const edge of block.edges) {
|
||||
const target = this.blocks[edge.dest];
|
||||
// start just below the source block
|
||||
edge.path.push(makeSegment([block.row + 1, block.col + 1], [block.row + 1, block.col + 1]));
|
||||
// horizontal segment over to main column
|
||||
edge.path.push(makeSegment([block.row + 1, block.col + 1], [block.row + 1, edge.mainColumn]));
|
||||
// vertical segment down the main column
|
||||
edge.path.push(makeSegment([block.row + 1, edge.mainColumn], [target.row, edge.mainColumn]));
|
||||
// horizontal segment over to the target column
|
||||
edge.path.push(makeSegment([target.row, edge.mainColumn], [target.row, target.col + 1]));
|
||||
// finish at the target block
|
||||
edge.path.push(makeSegment([target.row, target.col + 1], [target.row, target.col + 1]));
|
||||
// Simplify segments
|
||||
// Simplifications performed are eliminating (non-sentinel) edges which don't move anywhere and folding
|
||||
// VV -> V and HH -> H.
|
||||
let movement;
|
||||
do {
|
||||
movement = false;
|
||||
// i needs to start one into the range since we compare with i - 1
|
||||
for (let i = 1; i < edge.path.length; i++) {
|
||||
const prevSegment = edge.path[i - 1];
|
||||
const segment = edge.path[i];
|
||||
// sanity checks
|
||||
for (let j = 0; j < edge.path.length; j++) {
|
||||
const segment = edge.path[j];
|
||||
if (
|
||||
(segment.type === SegmentType.Vertical && segment.start.col !== segment.end.col) ||
|
||||
(segment.type === SegmentType.Horizontal && segment.start.row !== segment.end.row)
|
||||
) {
|
||||
throw Error("Segment type doesn't match coordinates");
|
||||
}
|
||||
if (j > 0) {
|
||||
const prev = edge.path[j - 1];
|
||||
if (prev.end.row !== segment.start.row || prev.end.col !== segment.start.col) {
|
||||
throw Error("Adjacent segment start/endpoints don't match");
|
||||
}
|
||||
}
|
||||
if (j < edge.path.length - 1) {
|
||||
const next = edge.path[j + 1];
|
||||
if (segment.end.row !== next.start.row || segment.end.col !== next.start.col) {
|
||||
throw Error("Adjacent segment start/endpoints don't match");
|
||||
}
|
||||
}
|
||||
}
|
||||
// If a segment doesn't go anywhere and is not a sentinel it can be eliminated
|
||||
if (
|
||||
segment.start.col === segment.end.col &&
|
||||
segment.start.row === segment.end.row &&
|
||||
i !== edge.path.length - 1
|
||||
) {
|
||||
edge.path.splice(i, 1);
|
||||
movement = true;
|
||||
continue;
|
||||
}
|
||||
// VV -> V
|
||||
// HH -> H
|
||||
if (prevSegment.type === segment.type) {
|
||||
if (
|
||||
(prevSegment.type === SegmentType.Vertical &&
|
||||
prevSegment.start.col !== segment.start.col) ||
|
||||
(prevSegment.type === SegmentType.Horizontal &&
|
||||
prevSegment.start.row !== segment.start.row)
|
||||
) {
|
||||
throw Error(
|
||||
"Adjacent horizontal or vertical segments don't share a common row or column"
|
||||
);
|
||||
}
|
||||
prevSegment.end = segment.end;
|
||||
edge.path.splice(i, 1);
|
||||
movement = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} while (movement);
|
||||
// sanity checks
|
||||
for (let j = 0; j < edge.path.length; j++) {
|
||||
const segment = edge.path[j];
|
||||
if (
|
||||
(segment.type === SegmentType.Vertical && segment.start.col !== segment.end.col) ||
|
||||
(segment.type === SegmentType.Horizontal && segment.start.row !== segment.end.row)
|
||||
) {
|
||||
throw Error("Segment type doesn't match coordinates (post-simplification)");
|
||||
}
|
||||
if (j > 0) {
|
||||
const prev = edge.path[j - 1];
|
||||
if (prev.end.row !== segment.start.row || prev.end.col !== segment.start.col) {
|
||||
throw Error("Adjacent segment start/endpoints don't match (post-simplification)");
|
||||
}
|
||||
}
|
||||
if (j < edge.path.length - 1) {
|
||||
const next = edge.path[j + 1];
|
||||
if (segment.end.row !== next.start.row || segment.end.col !== next.start.col) {
|
||||
throw Error("Adjacent segment start/endpoints don't match (post-simplification)");
|
||||
}
|
||||
}
|
||||
}
|
||||
// Compute subrows/subcolumns
|
||||
for (const segment of edge.path) {
|
||||
if (segment.type === SegmentType.Vertical) {
|
||||
if (segment.start.col !== segment.end.col) {
|
||||
throw Error('Vertical segment changes column');
|
||||
}
|
||||
const col = this.edgeColumns[segment.start.col];
|
||||
let inserted = false;
|
||||
for (const tree of col.intervals) {
|
||||
if (!tree.intersect_any([segment.start.row, segment.end.row])) {
|
||||
tree.insert([segment.start.row, segment.end.row], segment);
|
||||
inserted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!inserted) {
|
||||
const tree = new IntervalTree<EdgeSegment>();
|
||||
col.intervals.push(tree);
|
||||
col.subcolumns++;
|
||||
tree.insert([segment.start.row, segment.end.row], segment);
|
||||
}
|
||||
} else {
|
||||
// horizontal
|
||||
if (segment.start.row !== segment.end.row) {
|
||||
throw Error('Horizontal segment changes row');
|
||||
}
|
||||
const row = this.edgeRows[segment.start.row];
|
||||
let inserted = false;
|
||||
for (const tree of row.intervals) {
|
||||
if (!tree.intersect_any([segment.start.col, segment.end.col])) {
|
||||
tree.insert([segment.start.col, segment.end.col], segment);
|
||||
inserted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!inserted) {
|
||||
const tree = new IntervalTree<EdgeSegment>();
|
||||
row.intervals.push(tree);
|
||||
row.subrows++;
|
||||
tree.insert([segment.start.col, segment.end.col], segment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Throw everything away and do it all again, but smarter
|
||||
for (const edgeColumn of this.edgeColumns) {
|
||||
for (const intervalTree of edgeColumn.intervals) {
|
||||
intervalTree.root = null as unknown as Node<EdgeSegment>;
|
||||
}
|
||||
}
|
||||
for (const edgeRow of this.edgeRows) {
|
||||
for (const intervalTree of edgeRow.intervals) {
|
||||
intervalTree.root = null as unknown as Node<EdgeSegment>;
|
||||
}
|
||||
}
|
||||
// Edge kind is the primary heuristic for subrow/column assignment
|
||||
// For horizontal edges, think of left/vertical/right terminology rotated 90 degrees right
|
||||
enum EdgeKind {
|
||||
LEFTU = -2,
|
||||
LEFTCORNER = -1,
|
||||
VERTICAL = 0,
|
||||
RIGHTCORNER = 1,
|
||||
RIGHTU = 2,
|
||||
NULL = NaN,
|
||||
}
|
||||
const segments: {
|
||||
segment: EdgeSegment;
|
||||
length: number;
|
||||
kind: EdgeKind;
|
||||
tiebreaker: number;
|
||||
}[] = [];
|
||||
for (const block of this.blocks) {
|
||||
for (const edge of block.edges) {
|
||||
const edgeLength = edge.path
|
||||
.map(({start, end}) => Math.abs(start.col - end.col) + Math.abs(start.row - end.row))
|
||||
.reduce((A, x) => A + x);
|
||||
const target = this.blocks[edge.dest];
|
||||
for (const [i, segment] of edge.path.entries()) {
|
||||
let kind = EdgeKind.NULL;
|
||||
if (i === 0) {
|
||||
// segment will be vertical
|
||||
if (edge.path.length === 1) {
|
||||
kind = EdgeKind.VERTICAL;
|
||||
} else {
|
||||
const next = edge.path[i + 1];
|
||||
if (next.end.col > segment.end.col) {
|
||||
kind = EdgeKind.RIGHTCORNER;
|
||||
} else {
|
||||
kind = EdgeKind.LEFTCORNER;
|
||||
}
|
||||
}
|
||||
} else if (i === edge.path.length - 1) {
|
||||
// segment will be vertical
|
||||
// there will be a previous segment, i !== 0
|
||||
const previous = edge.path[i - 1];
|
||||
if (previous.start.col > segment.end.col) {
|
||||
kind = EdgeKind.RIGHTCORNER;
|
||||
} else {
|
||||
kind = EdgeKind.LEFTCORNER;
|
||||
}
|
||||
} else {
|
||||
// there will be both a previous and a next
|
||||
const next = edge.path[i + 1];
|
||||
const previous = edge.path[i - 1];
|
||||
if (segment.type === SegmentType.Vertical) {
|
||||
if (previous.start.col < segment.start.col && next.end.col < segment.start.col) {
|
||||
kind = EdgeKind.LEFTU;
|
||||
} else if (previous.start.col > segment.start.col && next.end.col > segment.start.col) {
|
||||
kind = EdgeKind.RIGHTU;
|
||||
} else if (previous.start.col > segment.end.col) {
|
||||
kind = EdgeKind.RIGHTCORNER;
|
||||
} else {
|
||||
kind = EdgeKind.LEFTCORNER;
|
||||
}
|
||||
} else {
|
||||
// horizontal
|
||||
// Same logic, think rotated 90 degrees right
|
||||
if (previous.start.row <= segment.start.row && next.end.row < segment.start.row) {
|
||||
kind = EdgeKind.LEFTU;
|
||||
} else if (previous.start.row > segment.start.row && next.end.row > segment.start.row) {
|
||||
kind = EdgeKind.RIGHTU;
|
||||
} else if (previous.start.row > segment.end.row) {
|
||||
kind = EdgeKind.RIGHTCORNER;
|
||||
} else {
|
||||
kind = EdgeKind.LEFTCORNER;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert((kind as any) !== EdgeKind.NULL);
|
||||
segments.push({
|
||||
segment,
|
||||
kind,
|
||||
length:
|
||||
Math.abs(segment.start.col - segment.end.col) +
|
||||
Math.abs(segment.start.row - segment.end.row),
|
||||
tiebreaker: 2 * edgeLength + (target.row >= block.row ? 1 : 0),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
segments.sort((a, b) => {
|
||||
if (a.kind !== b.kind) {
|
||||
return a.kind - b.kind;
|
||||
} else {
|
||||
const kind = a.kind; // a.kind == b.kind
|
||||
if (a.length !== b.length) {
|
||||
if (kind <= 0) {
|
||||
// shortest first if coming from the left
|
||||
return a.length - b.length;
|
||||
} else {
|
||||
// coming from the right, shortest last
|
||||
// reverse edge length order
|
||||
return b.length - a.length;
|
||||
}
|
||||
} else {
|
||||
if (kind <= 0) {
|
||||
return a.tiebreaker - b.tiebreaker;
|
||||
} else {
|
||||
// coming from the right, reverse
|
||||
return b.tiebreaker - a.tiebreaker;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
///console.log(segments);
|
||||
for (const segmentEntry of segments) {
|
||||
const {segment} = segmentEntry;
|
||||
if (segment.type === SegmentType.Vertical) {
|
||||
const col = this.edgeColumns[segment.start.col];
|
||||
let inserted = false;
|
||||
for (const tree of col.intervals) {
|
||||
if (!tree.intersect_any([segment.start.row, segment.end.row])) {
|
||||
tree.insert([segment.start.row, segment.end.row], segment);
|
||||
inserted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!inserted) {
|
||||
throw Error("Vertical segment couldn't be inserted");
|
||||
}
|
||||
} else {
|
||||
// Horizontal
|
||||
const row = this.edgeRows[segment.start.row];
|
||||
let inserted = false;
|
||||
for (const tree of row.intervals) {
|
||||
if (!tree.intersect_any([segment.start.col, segment.end.col])) {
|
||||
tree.insert([segment.start.col, segment.end.col], segment);
|
||||
inserted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!inserted) {
|
||||
throw Error("Horizontal segment couldn't be inserted");
|
||||
}
|
||||
}
|
||||
}
|
||||
// Assign offsets
|
||||
for (const edgeColumn of this.edgeColumns) {
|
||||
edgeColumn.width = Math.max(EDGE_SPACING + edgeColumn.intervals.length * EDGE_SPACING, 2 * EDGE_SPACING);
|
||||
for (const [i, intervalTree] of edgeColumn.intervals.entries()) {
|
||||
for (const segment of intervalTree.values) {
|
||||
segment.horizontalOffset = EDGE_SPACING * (i + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const edgeRow of this.edgeRows) {
|
||||
edgeRow.height = Math.max(EDGE_SPACING + edgeRow.intervals.length * EDGE_SPACING, 2 * EDGE_SPACING);
|
||||
for (const [i, intervalTree] of edgeRow.intervals.entries()) {
|
||||
for (const segment of intervalTree.values) {
|
||||
segment.verticalOffset = EDGE_SPACING * (i + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-statements
|
||||
computeCoordinates() {
|
||||
// Compute block row widths and heights
|
||||
for (const block of this.blocks) {
|
||||
// Update block width if it has a ton of incoming edges
|
||||
block.data.width = Math.max(block.data.width, (block.incidentEdgeCount - 1) * EDGE_SPACING);
|
||||
//console.log(this.blockRows[block.row].height, block.data.height, block.row);
|
||||
//console.log(this.blockRows);
|
||||
const halfWidth = (block.data.width - this.edgeColumns[block.col + 1].width) / 2;
|
||||
//console.log("--->", block.col, this.columnCount);
|
||||
this.blockRows[block.row].height = Math.max(this.blockRows[block.row].height, block.data.height);
|
||||
this.blockColumns[block.col].width = Math.max(this.blockColumns[block.col].width, halfWidth);
|
||||
this.blockColumns[block.col + 1].width = Math.max(this.blockColumns[block.col + 1].width, halfWidth);
|
||||
}
|
||||
// Compute row total offsets
|
||||
for (let i = 0; i < this.rowCount; i++) {
|
||||
// edge row 0 is already at the correct offset, this iteration will set the offset for block row 0 and edge
|
||||
// row 1.
|
||||
this.blockRows[i].totalOffset = this.edgeRows[i].totalOffset + this.edgeRows[i].height;
|
||||
this.edgeRows[i + 1].totalOffset = this.blockRows[i].totalOffset + this.blockRows[i].height;
|
||||
}
|
||||
// Compute column total offsets
|
||||
for (let i = 0; i < this.columnCount; i++) {
|
||||
// same deal here
|
||||
this.blockColumns[i].totalOffset = this.edgeColumns[i].totalOffset + this.edgeColumns[i].width;
|
||||
this.edgeColumns[i + 1].totalOffset = this.blockColumns[i].totalOffset + this.blockColumns[i].width;
|
||||
}
|
||||
// Compute block coordinates and edge paths
|
||||
for (const block of this.blocks) {
|
||||
block.coordinates.x =
|
||||
this.edgeColumns[block.col + 1].totalOffset -
|
||||
(block.data.width - this.edgeColumns[block.col + 1].width) / 2;
|
||||
block.coordinates.y = this.blockRows[block.row].totalOffset;
|
||||
for (const edge of block.edges) {
|
||||
if (edge.path.length === 1) {
|
||||
// Special case: Direct dropdown
|
||||
const segment = edge.path[0];
|
||||
const target = this.blocks[edge.dest];
|
||||
segment.start.x = this.edgeColumns[segment.start.col].totalOffset + segment.horizontalOffset;
|
||||
segment.start.y = block.coordinates.y + block.data.height;
|
||||
segment.end.x = this.edgeColumns[segment.end.col].totalOffset + segment.horizontalOffset;
|
||||
segment.end.y = this.edgeRows[target.row].totalOffset + this.edgeRows[target.row].height;
|
||||
} else {
|
||||
// push initial point
|
||||
{
|
||||
const segment = edge.path[0];
|
||||
segment.start.x = this.edgeColumns[segment.start.col].totalOffset + segment.horizontalOffset;
|
||||
segment.start.y = block.coordinates.y + block.data.height;
|
||||
segment.end.x = this.edgeColumns[segment.end.col].totalOffset + segment.horizontalOffset;
|
||||
segment.end.y = 0; // this is something we need from the next segment
|
||||
}
|
||||
// first and last handled specially
|
||||
for (const segment of edge.path.slice(1, edge.path.length - 1)) {
|
||||
segment.start.x = this.edgeColumns[segment.start.col].totalOffset + segment.horizontalOffset;
|
||||
segment.start.y = this.edgeRows[segment.start.row].totalOffset + segment.verticalOffset;
|
||||
segment.end.x = this.edgeColumns[segment.end.col].totalOffset + segment.horizontalOffset;
|
||||
segment.end.y = this.edgeRows[segment.end.row].totalOffset + segment.verticalOffset;
|
||||
}
|
||||
// push final point
|
||||
{
|
||||
const target = this.blocks[edge.dest];
|
||||
const segment = edge.path[edge.path.length - 1];
|
||||
segment.start.x = this.edgeColumns[segment.start.col].totalOffset + segment.horizontalOffset;
|
||||
segment.start.y = 0; // something we need from the previous segment
|
||||
segment.end.x = this.edgeColumns[segment.start.col].totalOffset + segment.horizontalOffset;
|
||||
segment.end.y = this.edgeRows[target.row].totalOffset + this.edgeRows[target.row].height;
|
||||
}
|
||||
// apply offsets to neighbor segments
|
||||
for (let i = 0; i < edge.path.length; i++) {
|
||||
const segment = edge.path[i];
|
||||
if (segment.type === SegmentType.Vertical) {
|
||||
if (i > 0) {
|
||||
const prev = edge.path[i - 1];
|
||||
prev.end.x = segment.start.x;
|
||||
}
|
||||
if (i < edge.path.length - 1) {
|
||||
const next = edge.path[i + 1];
|
||||
next.start.x = segment.end.x;
|
||||
}
|
||||
} else {
|
||||
// Horizontal
|
||||
if (i > 0) {
|
||||
const prev = edge.path[i - 1];
|
||||
prev.end.y = segment.start.y;
|
||||
}
|
||||
if (i < edge.path.length - 1) {
|
||||
const next = edge.path[i + 1];
|
||||
next.start.y = segment.end.y;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
layout() {
|
||||
const topologicalOrder = this.computeDag();
|
||||
//console.log(topologicalOrder);
|
||||
this.assignRows(topologicalOrder);
|
||||
//console.log(this.blocks);
|
||||
this.computeTree(topologicalOrder);
|
||||
//console.log(this.blocks);
|
||||
this.assignColumns(topologicalOrder);
|
||||
//console.log(this.blocks);
|
||||
this.setupRowsAndColumns();
|
||||
// Edge routing
|
||||
this.computeEdgeMainColumns();
|
||||
this.addEdgePaths();
|
||||
// -- Nothing is pixel aware above this line ---
|
||||
// Add pixel coordinates
|
||||
this.computeCoordinates();
|
||||
//
|
||||
///console.log(this);
|
||||
}
|
||||
|
||||
getWidth() {
|
||||
const lastCol = this.edgeColumns[this.edgeColumns.length - 1];
|
||||
return lastCol.totalOffset + lastCol.width;
|
||||
}
|
||||
|
||||
getHeight() {
|
||||
const lastRow = this.edgeRows[this.edgeRows.length - 1];
|
||||
return lastRow.totalOffset + lastRow.height;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
@import '~@fortawesome/fontawesome-free/css/all.min.css';
|
||||
@import '~vis-network/styles/vis-network.css';
|
||||
|
||||
html body {
|
||||
overflow: auto;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2021, Compiler Explorer Authors
|
||||
// Copyright (c) 2022, Compiler Explorer Authors
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
@@ -22,8 +22,14 @@
|
||||
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
// POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import {PaneState} from './pane.interfaces';
|
||||
import * as vis from 'vis-network';
|
||||
export interface CfgState {
|
||||
selectedFunction: string | null;
|
||||
zoom: number;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Previous state objects looked like:
|
||||
|
||||
export interface CfgOptions {
|
||||
physics?: boolean;
|
||||
@@ -36,3 +42,5 @@ export interface CfgState extends PaneState {
|
||||
scale: number;
|
||||
options?: CfgOptions;
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2017, Najjar Chedy
|
||||
// Copyright (c) 2022, Compiler Explorer Authors
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
@@ -22,427 +22,332 @@
|
||||
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
// POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import $ from 'jquery';
|
||||
import * as vis from 'vis-network';
|
||||
import _ from 'underscore';
|
||||
import {ga} from '../analytics';
|
||||
import {Toggles} from '../widgets/toggles';
|
||||
import TomSelect from 'tom-select';
|
||||
import {Container} from 'golden-layout';
|
||||
import {CfgOptions, CfgState} from './cfg-view.interfaces';
|
||||
import {Hub} from '../hub';
|
||||
import {Pane} from './pane';
|
||||
import * as monaco from 'monaco-editor';
|
||||
import $ from 'jquery';
|
||||
import _ from 'underscore';
|
||||
|
||||
interface NodeInfo {
|
||||
edges: string[];
|
||||
dagEdges: string[];
|
||||
index: string;
|
||||
id: number;
|
||||
level: number;
|
||||
state: number;
|
||||
inCount: number;
|
||||
}
|
||||
import {CfgState} from './cfg-view.interfaces';
|
||||
import {Hub} from '../hub';
|
||||
import {Container} from 'golden-layout';
|
||||
import {PaneState} from './pane.interfaces';
|
||||
import {ga} from '../analytics';
|
||||
import * as utils from '../utils';
|
||||
|
||||
import {
|
||||
AnnotatedCfgDescriptor,
|
||||
AnnotatedNodeDescriptor,
|
||||
CfgDescriptor,
|
||||
CFGResult,
|
||||
} from '../../types/compilation/cfg.interfaces';
|
||||
import {GraphLayoutCore} from '../graph-layout-core';
|
||||
import * as MonacoConfig from '../monaco-config';
|
||||
import TomSelect from 'tom-select';
|
||||
|
||||
const ColorTable = {
|
||||
red: '#FE5D5D',
|
||||
green: '#76E381',
|
||||
blue: '#65B7F6',
|
||||
grey: '#c5c5c5',
|
||||
};
|
||||
|
||||
type Coordinate = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
const DZOOM = 0.1;
|
||||
const MINZOOM = 0.1;
|
||||
|
||||
export class Cfg extends Pane<CfgState> {
|
||||
defaultCfgOutput: object;
|
||||
llvmCfgPlaceholder: object;
|
||||
binaryModeSupport: object;
|
||||
savedPos: any;
|
||||
savedScale: any;
|
||||
needsMove: boolean;
|
||||
currentFunc: string;
|
||||
functions: Record<string, vis.Data>;
|
||||
networkOpts: vis.Options;
|
||||
cfgVisualiser: vis.Network;
|
||||
_binaryFilter: boolean;
|
||||
functionPicker: TomSelect;
|
||||
toggles: Toggles;
|
||||
toggleNavigationButton: JQuery;
|
||||
toggleNavigationTitle: string;
|
||||
togglePhysicsButton: JQuery;
|
||||
togglePhysicsTitle: string;
|
||||
options: Required<CfgOptions>;
|
||||
graphDiv: HTMLElement;
|
||||
svg: SVGElement;
|
||||
blockContainer: HTMLElement;
|
||||
graphContainer: HTMLElement;
|
||||
graphElement: HTMLElement;
|
||||
infoElement: HTMLElement;
|
||||
currentPosition: Coordinate = {x: 0, y: 0};
|
||||
dragging = false;
|
||||
dragStart: Coordinate = {x: 0, y: 0};
|
||||
dragStartPosition: Coordinate = {x: 0, y: 0};
|
||||
graphDimensions = {width: 0, height: 0};
|
||||
functionSelector: TomSelect;
|
||||
results: CFGResult;
|
||||
state: CfgState & PaneState;
|
||||
layout: GraphLayoutCore;
|
||||
bbMap: Record<string, HTMLDivElement> = {};
|
||||
|
||||
constructor(hub: Hub, container: Container, state: CfgState) {
|
||||
constructor(hub: Hub, container: Container, state: CfgState & PaneState) {
|
||||
if ((state as any).selectedFn) {
|
||||
state = {
|
||||
id: state.id,
|
||||
compilerName: state.compilerName,
|
||||
editorid: state.editorid,
|
||||
treeid: state.treeid,
|
||||
selectedFunction: (state as any).selectedFn,
|
||||
zoom: 1,
|
||||
};
|
||||
}
|
||||
super(hub, container, state);
|
||||
|
||||
this.llvmCfgPlaceholder = {
|
||||
nodes: [
|
||||
{
|
||||
id: 0,
|
||||
shape: 'box',
|
||||
label: '-emit-llvm currently not supported',
|
||||
},
|
||||
],
|
||||
edges: [],
|
||||
};
|
||||
this.binaryModeSupport = {
|
||||
nodes: [
|
||||
{
|
||||
id: 0,
|
||||
shape: 'box',
|
||||
label: 'Cfg mode cannot be used when the binary filter is set',
|
||||
},
|
||||
],
|
||||
edges: [],
|
||||
};
|
||||
|
||||
this.savedPos = state.pos;
|
||||
this.savedScale = state.scale;
|
||||
this.needsMove = this.savedPos && this.savedScale;
|
||||
|
||||
this.currentFunc = state.selectedFn || '';
|
||||
this.functions = {};
|
||||
|
||||
this._binaryFilter = false;
|
||||
|
||||
const pickerEl = this.domRoot.find('.function-picker')[0] as HTMLInputElement;
|
||||
this.functionPicker = new TomSelect(pickerEl, {
|
||||
sortField: 'name',
|
||||
valueField: 'name',
|
||||
labelField: 'name',
|
||||
searchField: ['name'],
|
||||
this.eventHub.emit('cfgViewOpened', this.compilerInfo.compilerId);
|
||||
this.eventHub.emit('requestFilters', this.compilerInfo.compilerId);
|
||||
this.eventHub.emit('requestCompiler', this.compilerInfo.compilerId);
|
||||
const selector = this.domRoot.get()[0].getElementsByClassName('function-selector')[0];
|
||||
if (!(selector instanceof HTMLSelectElement)) {
|
||||
throw new Error('.function-selector is not an HTMLSelectElement');
|
||||
}
|
||||
this.functionSelector = new TomSelect(selector, {
|
||||
valueField: 'value',
|
||||
labelField: 'title',
|
||||
searchField: ['title'],
|
||||
placeholder: '🔍 Select a function...',
|
||||
dropdownParent: 'body',
|
||||
plugins: ['input_autogrow'],
|
||||
onChange: (e: any) => {
|
||||
// TomSelect says it's an Event, but we receive strings
|
||||
const val = e as string;
|
||||
if (val in this.functions) {
|
||||
const selectedFn = this.functions[val];
|
||||
this.currentFunc = val;
|
||||
this.showCfgResults({
|
||||
nodes: selectedFn.nodes,
|
||||
edges: selectedFn.edges,
|
||||
});
|
||||
if (selectedFn.nodes && selectedFn.nodes.length > 0) {
|
||||
this.cfgVisualiser.selectNodes([selectedFn.nodes[0].id]);
|
||||
}
|
||||
this.resize();
|
||||
this.updateState();
|
||||
}
|
||||
plugins: ['dropdown_input'],
|
||||
sortField: 'title',
|
||||
onChange: e => {
|
||||
this.selectFunction(e as any as string);
|
||||
},
|
||||
});
|
||||
|
||||
this.updateButtons();
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
override getInitialHTML(): string {
|
||||
override getInitialHTML() {
|
||||
return $('#cfg').html();
|
||||
}
|
||||
|
||||
override getDefaultPaneName() {
|
||||
return 'CFG';
|
||||
}
|
||||
|
||||
override registerOpeningAnalyticsEvent(): void {
|
||||
ga.proxy('send', {
|
||||
hitType: 'event',
|
||||
eventCategory: 'OpenViewPane',
|
||||
eventAction: 'Cfg',
|
||||
eventAction: 'CFGViewPane',
|
||||
});
|
||||
}
|
||||
|
||||
override onCompileResult(compilerId: number, compiler: any, result: any) {
|
||||
if (this.compilerInfo.compilerId === compilerId) {
|
||||
let functionNames: string[] = [];
|
||||
if (compiler.supportsCfg && !$.isEmptyObject(result.cfg)) {
|
||||
this.functions = result.cfg;
|
||||
functionNames = Object.keys(this.functions);
|
||||
if (functionNames.indexOf(this.currentFunc) === -1) {
|
||||
this.currentFunc = functionNames[0];
|
||||
}
|
||||
const selectedFn = this.functions[this.currentFunc];
|
||||
this.showCfgResults({
|
||||
nodes: selectedFn.nodes,
|
||||
edges: selectedFn.edges,
|
||||
});
|
||||
if (selectedFn.nodes && selectedFn.nodes.length > 0) {
|
||||
this.cfgVisualiser.selectNodes([selectedFn.nodes[0].id]);
|
||||
}
|
||||
} else {
|
||||
// We don't reset the current function here as we would lose the saved one if this happened at the beginning
|
||||
// (Hint: It *does* happen)
|
||||
if (!result.compilationOptions?.includes('-emit-llvm')) {
|
||||
this.showCfgResults(this._binaryFilter ? this.binaryModeSupport : this.defaultCfgOutput);
|
||||
} else {
|
||||
this.showCfgResults(this._binaryFilter ? this.binaryModeSupport : this.llvmCfgPlaceholder);
|
||||
}
|
||||
}
|
||||
|
||||
this.functionPicker.clearOptions();
|
||||
this.functionPicker.addOption(
|
||||
functionNames.length
|
||||
? this.adaptStructure(functionNames)
|
||||
: {name: 'The input does not contain functions'}
|
||||
);
|
||||
this.functionPicker.refreshOptions(false);
|
||||
|
||||
this.functionPicker.clear();
|
||||
this.functionPicker.addItem(
|
||||
functionNames.length ? this.currentFunc : 'The input does not contain any function',
|
||||
true
|
||||
);
|
||||
this.updateState();
|
||||
}
|
||||
}
|
||||
|
||||
override registerDynamicElements(state: CfgState) {
|
||||
this.defaultCfgOutput = {nodes: [{id: 0, shape: 'box', label: 'No Output'}], edges: []};
|
||||
// Note that this might be outdated if no functions were present when creating the link, but that's handled
|
||||
// by selectize
|
||||
this.options = {
|
||||
navigation: state.options?.navigation ?? false,
|
||||
physics: state.options?.physics ?? false,
|
||||
};
|
||||
|
||||
this.networkOpts = {
|
||||
autoResize: true,
|
||||
locale: 'en',
|
||||
edges: {
|
||||
arrows: {to: {enabled: true}},
|
||||
smooth: {
|
||||
enabled: true,
|
||||
type: 'dynamic',
|
||||
roundness: 1,
|
||||
},
|
||||
physics: true,
|
||||
},
|
||||
nodes: {
|
||||
font: {face: 'Consolas, "Liberation Mono", Courier, monospace', align: 'left'},
|
||||
},
|
||||
layout: {
|
||||
hierarchical: {
|
||||
enabled: true,
|
||||
direction: 'UD',
|
||||
nodeSpacing: 100,
|
||||
levelSeparation: 150,
|
||||
},
|
||||
},
|
||||
physics: {
|
||||
enabled: this.options.physics,
|
||||
hierarchicalRepulsion: {
|
||||
nodeDistance: 160,
|
||||
},
|
||||
},
|
||||
interaction: {
|
||||
navigationButtons: this.options.navigation,
|
||||
keyboard: {
|
||||
enabled: true,
|
||||
speed: {x: 10, y: 10, zoom: 0.03},
|
||||
bindToWindow: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
this.cfgVisualiser = new vis.Network(
|
||||
this.domRoot.find('.graph-placeholder')[0],
|
||||
this.defaultCfgOutput,
|
||||
this.networkOpts
|
||||
);
|
||||
}
|
||||
|
||||
override onCompiler(compilerId: number, compiler: any) {
|
||||
if (compilerId === this.compilerInfo.compilerId) {
|
||||
this.compilerInfo.compilerName = compiler ? compiler.name : '';
|
||||
this.updateTitle();
|
||||
}
|
||||
}
|
||||
|
||||
onFiltersChange(compilerId: number, filters: any) {
|
||||
if (this.compilerInfo.compilerId === compilerId) {
|
||||
this._binaryFilter = filters.binary;
|
||||
}
|
||||
}
|
||||
|
||||
override registerButtons(state: CfgState) {
|
||||
this.toggles = new Toggles(this.domRoot.find('.options'), this.options);
|
||||
|
||||
this.toggleNavigationButton = this.domRoot.find('.toggle-navigation');
|
||||
this.toggleNavigationTitle = this.toggleNavigationButton.prop('title') as string;
|
||||
|
||||
this.togglePhysicsButton = this.domRoot.find('.toggle-physics');
|
||||
this.togglePhysicsTitle = this.togglePhysicsButton.prop('title');
|
||||
|
||||
this.topBar = this.domRoot.find('.top-bar');
|
||||
this.graphDiv = this.domRoot.find('.graph')[0];
|
||||
this.svg = this.domRoot.find('svg')[0] as SVGElement;
|
||||
this.blockContainer = this.domRoot.find('.block-container')[0];
|
||||
this.graphContainer = this.domRoot.find('.graph-container')[0];
|
||||
this.graphElement = this.domRoot.find('.graph')[0];
|
||||
this.infoElement = this.domRoot.find('.cfg-info')[0];
|
||||
}
|
||||
|
||||
override registerCallbacks() {
|
||||
this.cfgVisualiser.on('dragEnd', this.updateState.bind(this));
|
||||
this.cfgVisualiser.on('zoom', this.updateState.bind(this));
|
||||
|
||||
this.eventHub.on('filtersChange', this.onFiltersChange, this);
|
||||
|
||||
this.eventHub.emit('cfgViewOpened', this.compilerInfo.compilerId);
|
||||
this.eventHub.emit('requestFilters', this.compilerInfo.compilerId);
|
||||
this.eventHub.emit('requestCompiler', this.compilerInfo.compilerId);
|
||||
|
||||
this.togglePhysicsButton.on('click', () => {
|
||||
this.networkOpts.physics.enabled = this.togglePhysicsButton.hasClass('active');
|
||||
// change only physics.enabled option to preserve current node locations
|
||||
this.cfgVisualiser.setOptions({
|
||||
physics: {enabled: this.networkOpts.physics.enabled},
|
||||
});
|
||||
this.graphContainer.addEventListener('mousedown', e => {
|
||||
const div = (e.target as Element).closest('div');
|
||||
if (div && (div.classList.contains('block-container') || div.classList.contains('graph-container'))) {
|
||||
this.dragging = true;
|
||||
this.dragStart = {x: e.clientX, y: e.clientY};
|
||||
this.dragStartPosition = {...this.currentPosition};
|
||||
} else {
|
||||
// pass, let the user select block contents and other text
|
||||
}
|
||||
});
|
||||
|
||||
this.toggleNavigationButton.on('click', () => {
|
||||
this.networkOpts.interaction.navigationButtons = this.toggleNavigationButton.hasClass('active');
|
||||
this.cfgVisualiser.setOptions({
|
||||
interaction: {
|
||||
navigationButtons: this.networkOpts.interaction.navigationButtons,
|
||||
},
|
||||
});
|
||||
this.graphContainer.addEventListener('mouseup', e => {
|
||||
this.dragging = false;
|
||||
});
|
||||
this.toggles.on('change', () => {
|
||||
this.updateButtons();
|
||||
this.updateState();
|
||||
this.graphContainer.addEventListener('mousemove', e => {
|
||||
if (this.dragging) {
|
||||
this.currentPosition = {
|
||||
x: e.clientX - this.dragStart.x + this.dragStartPosition.x,
|
||||
y: e.clientY - this.dragStart.y + this.dragStartPosition.y,
|
||||
};
|
||||
this.graphElement.style.left = this.currentPosition.x + 'px';
|
||||
this.graphElement.style.top = this.currentPosition.y + 'px';
|
||||
}
|
||||
});
|
||||
this.graphContainer.addEventListener('wheel', e => {
|
||||
const delta = DZOOM * -Math.sign(e.deltaY) * Math.max(1, this.state.zoom - 1);
|
||||
const prevZoom = this.state.zoom;
|
||||
this.state.zoom += delta;
|
||||
if (this.state.zoom >= MINZOOM) {
|
||||
this.graphElement.style.transform = `scale(${this.state.zoom})`;
|
||||
const mouseX = e.clientX - this.graphElement.getBoundingClientRect().x;
|
||||
const mouseY = e.clientY - this.graphElement.getBoundingClientRect().y;
|
||||
// Amount that the zoom will offset is mouseX / width before zoom * delta * unzoomed width
|
||||
// And same for y. The width / height terms cancel.
|
||||
this.currentPosition.x -= (mouseX / prevZoom) * delta;
|
||||
this.currentPosition.y -= (mouseY / prevZoom) * delta;
|
||||
this.graphElement.style.left = this.currentPosition.x + 'px';
|
||||
this.graphElement.style.top = this.currentPosition.y + 'px';
|
||||
} else {
|
||||
this.state.zoom = MINZOOM;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateButtons() {
|
||||
const formatButtonTitle = (button: JQuery, title: string) => {
|
||||
button.prop('title', '[' + (button.hasClass('active') ? 'ON' : 'OFF') + '] ' + title);
|
||||
};
|
||||
formatButtonTitle(this.togglePhysicsButton, this.togglePhysicsTitle);
|
||||
formatButtonTitle(this.toggleNavigationButton, this.toggleNavigationTitle);
|
||||
override onCompiler(compilerId: number, compiler: any, options: unknown, editorId: number, treeId: number): void {
|
||||
if (this.compilerInfo.compilerId !== compilerId) return;
|
||||
this.compilerInfo.compilerName = compiler ? compiler.name : '';
|
||||
this.compilerInfo.editorId = editorId;
|
||||
this.compilerInfo.treeId = treeId;
|
||||
this.updateTitle();
|
||||
if (compiler && !compiler.supportsLLVMOptPipelineView) {
|
||||
//this.editor.setValue('<LLVM IR output is not supported for this compiler>');
|
||||
}
|
||||
}
|
||||
|
||||
override onCompileResult(compilerId: number, compiler: any, result: any) {
|
||||
if (this.compilerInfo.compilerId !== compilerId) return;
|
||||
this.functionSelector.clear(true);
|
||||
this.functionSelector.clearOptions();
|
||||
if (result.cfg) {
|
||||
const cfg = result.cfg as CFGResult;
|
||||
this.results = cfg;
|
||||
let selectedFunction: string | null = this.state.selectedFunction;
|
||||
const keys = Object.keys(cfg);
|
||||
if (keys.length === 0) {
|
||||
this.functionSelector.addOption({
|
||||
title: '<No functions available>',
|
||||
value: '<No functions available>',
|
||||
});
|
||||
}
|
||||
for (const fn of keys) {
|
||||
this.functionSelector.addOption({
|
||||
title: fn,
|
||||
value: fn,
|
||||
});
|
||||
}
|
||||
if (keys.length > 0) {
|
||||
if (selectedFunction === '' || !(selectedFunction !== null && selectedFunction in cfg)) {
|
||||
selectedFunction = keys[0];
|
||||
}
|
||||
this.functionSelector.setValue(selectedFunction, true);
|
||||
this.state.selectedFunction = selectedFunction;
|
||||
} else {
|
||||
// this.state.selectedFunction won't change, next time the compilation results aren't errors or empty
|
||||
// the selected function will still be the same
|
||||
selectedFunction = null;
|
||||
}
|
||||
this.selectFunction(selectedFunction);
|
||||
} else {
|
||||
// this case can be fallen into with a blank input file
|
||||
this.selectFunction(null);
|
||||
}
|
||||
}
|
||||
|
||||
async createBasicBlocks(fn: CfgDescriptor) {
|
||||
for (const node of fn.nodes) {
|
||||
const div = document.createElement('div');
|
||||
div.classList.add('block');
|
||||
div.innerHTML = await monaco.editor.colorize(node.label, 'asm', MonacoConfig.extendConfig({}));
|
||||
if (node.id in this.bbMap) {
|
||||
throw Error("Duplicate basic block node id's found while drawing cfg");
|
||||
}
|
||||
this.bbMap[node.id] = div;
|
||||
this.blockContainer.appendChild(div);
|
||||
}
|
||||
for (const node of fn.nodes) {
|
||||
const elem = $(this.bbMap[node.id]);
|
||||
void this.bbMap[node.id].offsetHeight;
|
||||
(node as AnnotatedNodeDescriptor).width = elem.outerWidth() as number;
|
||||
(node as AnnotatedNodeDescriptor).height = elem.outerHeight() as number;
|
||||
}
|
||||
}
|
||||
|
||||
drawEdges() {
|
||||
const width = this.layout.getWidth();
|
||||
const height = this.layout.getHeight();
|
||||
this.svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
||||
// We want to assembly everything in a document fragment first, then add it to the dom
|
||||
// If we add to the dom every iteration the performance is awful, presumably because of layout computation and
|
||||
// rendering and whatnot.
|
||||
const documentFragment = document.createDocumentFragment();
|
||||
for (const block of this.layout.blocks) {
|
||||
for (const edge of block.edges) {
|
||||
// Sanity check
|
||||
if (edge.path.length === 0) {
|
||||
throw Error('Mal-formed edge: Zero segments');
|
||||
}
|
||||
const points: [number, number][] = [];
|
||||
// -1 offset is to create an overlap between the block's bottom border and start of the path, avoid any
|
||||
// visual artifacts
|
||||
points.push([edge.path[0].start.x, edge.path[0].start.y - 1]);
|
||||
for (const segment of edge.path.slice(0, edge.path.length - 1)) {
|
||||
points.push([segment.end.x, segment.end.y]);
|
||||
}
|
||||
// Edge arrow is going to be a triangle
|
||||
const triangleHeight = 7;
|
||||
const triangleWidth = 7;
|
||||
const endpoint = edge.path[edge.path.length - 1].end;
|
||||
// +1 offset to create an overlap with the triangle
|
||||
points.push([endpoint.x, endpoint.y - triangleHeight + 1]);
|
||||
// Create the poly line
|
||||
const line = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
|
||||
line.setAttribute('points', points.map(coord => coord.join(',')).join(' '));
|
||||
line.setAttribute('fill', 'none');
|
||||
line.setAttribute('stroke', ColorTable[edge.color]);
|
||||
line.setAttribute('stroke-width', '2');
|
||||
documentFragment.appendChild(line);
|
||||
// Create teh triangle
|
||||
const trianglePoints: [number, number][] = [];
|
||||
trianglePoints.push([endpoint.x - triangleWidth / 2, endpoint.y - triangleHeight]);
|
||||
trianglePoints.push([endpoint.x + triangleWidth / 2, endpoint.y - triangleHeight]);
|
||||
trianglePoints.push([endpoint.x, endpoint.y]);
|
||||
trianglePoints.push([endpoint.x - triangleWidth / 2, endpoint.y - triangleHeight]);
|
||||
trianglePoints.push([endpoint.x + triangleWidth / 2, endpoint.y - triangleHeight]);
|
||||
const triangle = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
|
||||
triangle.setAttribute('points', trianglePoints.map(coord => coord.join(',')).join(' '));
|
||||
triangle.setAttribute('fill', ColorTable[edge.color]);
|
||||
documentFragment.appendChild(triangle);
|
||||
}
|
||||
}
|
||||
this.svg.appendChild(documentFragment);
|
||||
}
|
||||
|
||||
applyLayout() {
|
||||
const width = this.layout.getWidth();
|
||||
const height = this.layout.getHeight();
|
||||
this.graphDimensions.width = width;
|
||||
this.graphDimensions.height = height;
|
||||
this.graphDiv.style.height = height + 'px';
|
||||
this.graphDiv.style.width = width + 'px';
|
||||
this.svg.style.height = height + 'px';
|
||||
this.svg.style.width = width + 'px';
|
||||
this.blockContainer.style.height = height + 'px';
|
||||
this.blockContainer.style.width = width + 'px';
|
||||
for (const block of this.layout.blocks) {
|
||||
const elem = this.bbMap[block.data.id];
|
||||
elem.style.top = block.coordinates.y + 'px';
|
||||
elem.style.left = block.coordinates.x + 'px';
|
||||
elem.style.width = block.data.width + 'px';
|
||||
elem.style.height = block.data.height + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
// display the cfg for the specified function if it exists
|
||||
// this function does not change or use this.state.selectedFunction
|
||||
async selectFunction(name: string | null) {
|
||||
this.blockContainer.innerHTML = '';
|
||||
this.svg.innerHTML = '';
|
||||
if (!name || !(name in this.results)) {
|
||||
return;
|
||||
}
|
||||
const fn = this.results[name];
|
||||
this.bbMap = {};
|
||||
await this.createBasicBlocks(fn);
|
||||
this.layout = new GraphLayoutCore(fn as AnnotatedCfgDescriptor);
|
||||
this.applyLayout();
|
||||
this.drawEdges();
|
||||
this.infoElement.innerHTML = `Layout time: ${Math.round(this.layout.layoutTime)}ms<br/>Basic blocks: ${
|
||||
fn.nodes.length
|
||||
}`;
|
||||
}
|
||||
|
||||
override resize() {
|
||||
const height = (this.domRoot.height() as number) - (this.topBar.outerHeight(true) ?? 0);
|
||||
if ((this.cfgVisualiser as any).canvas !== undefined) {
|
||||
this.cfgVisualiser.setSize('100%', height.toString());
|
||||
this.cfgVisualiser.redraw();
|
||||
}
|
||||
}
|
||||
|
||||
override getDefaultPaneName() {
|
||||
return 'Graph Viewer';
|
||||
}
|
||||
|
||||
assignLevels(data: vis.Data) {
|
||||
const nodes: NodeInfo[] = [];
|
||||
const idToIdx: string[] = [];
|
||||
for (const i in data.nodes) {
|
||||
const node = data.nodes[i];
|
||||
idToIdx[node.id] = i;
|
||||
nodes.push({
|
||||
edges: [],
|
||||
dagEdges: [],
|
||||
index: i,
|
||||
id: node.id,
|
||||
level: 0,
|
||||
state: 0,
|
||||
inCount: 0,
|
||||
});
|
||||
}
|
||||
const isEdgeValid = (edge: vis.Edge) => edge.from && edge.to && edge.from in idToIdx && edge.to in idToIdx;
|
||||
for (const edge of data.edges as vis.Edge[]) {
|
||||
if (edge.from && edge.to && isEdgeValid(edge)) {
|
||||
nodes[idToIdx[edge.from]].edges.push(idToIdx[edge.to]);
|
||||
}
|
||||
}
|
||||
|
||||
const dfs = (node: NodeInfo) => {
|
||||
// choose which edges will be back-edges
|
||||
node.state = 1;
|
||||
|
||||
node.edges.forEach(targetIndex => {
|
||||
const target = nodes[targetIndex];
|
||||
if (target.state !== 1) {
|
||||
if (target.state === 0) {
|
||||
dfs(target);
|
||||
}
|
||||
node.dagEdges.push(targetIndex);
|
||||
target.inCount += 1;
|
||||
}
|
||||
});
|
||||
node.state = 2;
|
||||
};
|
||||
const markLevels = (node: NodeInfo) => {
|
||||
node.dagEdges.forEach(targetIndex => {
|
||||
const target = nodes[targetIndex];
|
||||
target.level = Math.max(target.level, node.level + 1);
|
||||
if (--target.inCount === 0) {
|
||||
markLevels(target);
|
||||
}
|
||||
});
|
||||
};
|
||||
nodes.forEach(node => {
|
||||
if (node.state === 0) {
|
||||
dfs(node);
|
||||
node.level = 1;
|
||||
markLevels(node);
|
||||
}
|
||||
_.defer(() => {
|
||||
const topBarHeight = utils.updateAndCalcTopBarHeight(this.domRoot, this.topBar, this.hideable);
|
||||
this.graphContainer.style.width = `${this.domRoot.width() as number}px`;
|
||||
this.graphContainer.style.height = `${(this.domRoot.height() as number) - topBarHeight}px`;
|
||||
});
|
||||
if (data.nodes) {
|
||||
for (const node of nodes) {
|
||||
data.nodes[node.index]['level'] = node.level;
|
||||
}
|
||||
}
|
||||
|
||||
for (const edge of data.edges as vis.Edge[]) {
|
||||
if (edge.from && edge.to && isEdgeValid(edge)) {
|
||||
const nodeA = nodes[idToIdx[edge.from]];
|
||||
const nodeB = nodes[idToIdx[edge.to]];
|
||||
if (nodeA.level >= nodeB.level) {
|
||||
edge.physics = false;
|
||||
} else {
|
||||
edge.physics = true;
|
||||
const diff = nodeB.level - nodeA.level;
|
||||
edge.length = diff * (200 - 5 * Math.min(5, diff));
|
||||
}
|
||||
} else {
|
||||
edge.physics = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showCfgResults(data: vis.Data) {
|
||||
this.assignLevels(data);
|
||||
this.cfgVisualiser.setData(data);
|
||||
/* FIXME: This does not work.
|
||||
* It's here because I suspected that not having content in the constructor was
|
||||
* breaking the move, but it does not seem like it
|
||||
*/
|
||||
if (this.needsMove) {
|
||||
this.cfgVisualiser.moveTo({
|
||||
position: this.savedPos,
|
||||
animation: false,
|
||||
scale: this.savedScale,
|
||||
});
|
||||
this.needsMove = false;
|
||||
}
|
||||
}
|
||||
|
||||
override onCompilerClose(compilerId: number) {
|
||||
if (this.compilerInfo.compilerId === compilerId) {
|
||||
// We can't immediately close as an outer loop somewhere in GoldenLayout is iterating over
|
||||
// the hierarchy. We can't modify while it's being iterated over.
|
||||
this.close();
|
||||
_.defer(() => {
|
||||
this.container.close();
|
||||
}, this);
|
||||
}
|
||||
}
|
||||
|
||||
override close() {
|
||||
override close(): void {
|
||||
this.eventHub.unsubscribe();
|
||||
this.eventHub.emit('cfgViewClosed', this.compilerInfo.compilerId);
|
||||
this.cfgVisualiser.destroy();
|
||||
}
|
||||
|
||||
getEffectiveOptions() {
|
||||
return this.toggles.get();
|
||||
}
|
||||
|
||||
override getCurrentState(): CfgState {
|
||||
return {
|
||||
...super.getCurrentState(),
|
||||
selectedFn: this.currentFunc,
|
||||
pos: this.cfgVisualiser.getViewPosition(),
|
||||
scale: this.cfgVisualiser.getScale(),
|
||||
options: this.getEffectiveOptions(),
|
||||
};
|
||||
}
|
||||
|
||||
adaptStructure(names: string[]) {
|
||||
return names.map(name => {
|
||||
return {name};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2158,7 +2158,6 @@ export class Compiler extends MonacoPane<monaco.editor.IStandaloneCodeEditor, Co
|
||||
filters: CompilerOutputOptions & Record<string, boolean | undefined>,
|
||||
reqCompile: boolean
|
||||
): void {
|
||||
|
||||
if (this.id === id) {
|
||||
this.treeDumpEnabled = filters.treeDump !== false;
|
||||
this.rtlDumpEnabled = filters.rtlDump !== false;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
@import '~@fortawesome/fontawesome-free/css/all.min.css';
|
||||
@import '~vis-network/styles/vis-network.css';
|
||||
|
||||
/*
|
||||
* https://github.com/Microsoft/monaco-editor/issues/417
|
||||
@@ -384,13 +383,44 @@ pre.content.wrap * {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.cfg-toolbar table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.graph-placeholder {
|
||||
.graph-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
.cfg-info {
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
left: 5px;
|
||||
font-size: x-small;
|
||||
font-style: italic;
|
||||
z-index: 1;
|
||||
}
|
||||
.graph {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform-origin: top left;
|
||||
svg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
.block-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
.block {
|
||||
position: absolute;
|
||||
padding: 5px;
|
||||
display: inline-block;
|
||||
// TODO(jeremy-rifkin) settings.editorsFont
|
||||
font-family: Consolas, 'Liberation Mono', Courier, monospace;
|
||||
white-space: nowrap;
|
||||
line-height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.clear-cache {
|
||||
|
||||
@@ -238,8 +238,15 @@ textarea.form-control {
|
||||
background-color: darken(#474747, 10%);
|
||||
}
|
||||
|
||||
.graph-placeholder {
|
||||
background-color: #1e1e1e !important;
|
||||
.graph-container {
|
||||
.cfg-info {
|
||||
color: #aaa;
|
||||
}
|
||||
.graph .block-container .block {
|
||||
background: black;
|
||||
border: 1px solid white;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.input-group-text {
|
||||
|
||||
@@ -204,8 +204,16 @@ a.navbar-brand img.logo.normal {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.graph-placeholder {
|
||||
background-color: #ffffff;
|
||||
.graph-container {
|
||||
background: rgb(245, 245, 245);
|
||||
.cfg-info {
|
||||
color: rgb(56, 56, 56);
|
||||
}
|
||||
.graph .block-container .block {
|
||||
background: white;
|
||||
border: 1px solid rgb(55, 55, 55);
|
||||
color: rgb(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.text-count {
|
||||
|
||||
2
test/cfg-cases/cfg-gcc.if-else.json
generated
2
test/cfg-cases/cfg-gcc.if-else.json
generated
@@ -1085,7 +1085,7 @@
|
||||
"nodes": [
|
||||
{
|
||||
"id": "_GLOBAL__sub_I_main:",
|
||||
"label": "_GLOBAL__sub_I_main:\n sub rsp, 8\n mov edi, OFFSET FLAT:std::__ioinit\n call std::ios_base::Init::Init()\n mov edx, OFFSET FLAT:__dso_handle\n mov esi, OFFSET FLAT:std::__ioinit\n mov edi, OFFSET FLAT:std::ios_base::Init::~Init(\n add rsp, 8\n jmp __cxa_atexit",
|
||||
"label": "_GLOBAL__sub_I_main:\n sub rsp, 8\n mov edi, OFFSET FLAT:std::__ioinit\n call std::ios_base::Init::Init()\n mov edx, OFFSET FLAT:__dso_handle\n mov esi, OFFSET FLAT:std::__ioinit\n mov edi, OFFSET FLAT:std::ios_base::Init::~Init()\n add rsp, 8\n jmp __cxa_atexit",
|
||||
"color": "#99ccff",
|
||||
"shape": "box"
|
||||
}
|
||||
|
||||
2
test/cfg-cases/cfg-gcc.loop.json
generated
2
test/cfg-cases/cfg-gcc.loop.json
generated
@@ -1169,7 +1169,7 @@
|
||||
"nodes": [
|
||||
{
|
||||
"id": "_GLOBAL__sub_I_main:",
|
||||
"label": "_GLOBAL__sub_I_main:\n sub rsp, 8\n mov edi, OFFSET FLAT:std::__ioinit\n call std::ios_base::Init::Init()\n mov edx, OFFSET FLAT:__dso_handle\n mov esi, OFFSET FLAT:std::__ioinit\n mov edi, OFFSET FLAT:std::ios_base::Init::~Init(\n add rsp, 8\n jmp __cxa_atexit",
|
||||
"label": "_GLOBAL__sub_I_main:\n sub rsp, 8\n mov edi, OFFSET FLAT:std::__ioinit\n call std::ios_base::Init::Init()\n mov edx, OFFSET FLAT:__dso_handle\n mov esi, OFFSET FLAT:std::__ioinit\n mov edi, OFFSET FLAT:std::ios_base::Init::~Init()\n add rsp, 8\n jmp __cxa_atexit",
|
||||
"color": "#99ccff",
|
||||
"shape": "box"
|
||||
}
|
||||
|
||||
2
test/cfg-cases/cfg-gcc.single-block.json
generated
2
test/cfg-cases/cfg-gcc.single-block.json
generated
@@ -99,7 +99,7 @@
|
||||
"nodes": [
|
||||
{
|
||||
"id": "_GLOBAL__sub_I_main:",
|
||||
"label": "_GLOBAL__sub_I_main:\n sub rsp, 8\n mov edi, OFFSET FLAT:std::__ioinit\n call std::ios_base::Init::Init()\n mov edx, OFFSET FLAT:__dso_handle\n mov esi, OFFSET FLAT:std::__ioinit\n mov edi, OFFSET FLAT:std::ios_base::Init::~Init(\n add rsp, 8\n jmp __cxa_atexit",
|
||||
"label": "_GLOBAL__sub_I_main:\n sub rsp, 8\n mov edi, OFFSET FLAT:std::__ioinit\n call std::ios_base::Init::Init()\n mov edx, OFFSET FLAT:__dso_handle\n mov esi, OFFSET FLAT:std::__ioinit\n mov edi, OFFSET FLAT:std::ios_base::Init::~Init()\n add rsp, 8\n jmp __cxa_atexit",
|
||||
"color": "#99ccff",
|
||||
"shape": "box"
|
||||
}
|
||||
|
||||
56
types/compilation/cfg.interfaces.ts
Normal file
56
types/compilation/cfg.interfaces.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) 2022, Compiler Explorer Authors
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// * Redistributions of source code must retain the above copyright notice,
|
||||
// this list of conditions and the following disclaimer.
|
||||
// * Redistributions in binary form must reproduce the above copyright
|
||||
// notice, this list of conditions and the following disclaimer in the
|
||||
// documentation and/or other materials provided with the distribution.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
// POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
// TODO(jeremy-rifkin): re-visit all the types here once the back-end is more typescripted
|
||||
|
||||
export type EdgeDescriptor = {
|
||||
from: string;
|
||||
to: string;
|
||||
arrows: string; // <- useless
|
||||
color: string;
|
||||
};
|
||||
|
||||
export type NodeDescriptor = {
|
||||
color: string; // <- useless
|
||||
id: string; // typically label for the bb
|
||||
label: string; // really the source
|
||||
shape: string; // <- useless
|
||||
};
|
||||
|
||||
export type AnnotatedNodeDescriptor = NodeDescriptor & {
|
||||
width: number; // in pixels
|
||||
height: number; // in pixels
|
||||
};
|
||||
|
||||
type CfgDescriptor_<ND> = {
|
||||
edges: EdgeDescriptor[];
|
||||
nodes: ND[];
|
||||
};
|
||||
|
||||
export type CfgDescriptor = CfgDescriptor_<NodeDescriptor>;
|
||||
export type AnnotatedCfgDescriptor = CfgDescriptor_<AnnotatedNodeDescriptor>;
|
||||
|
||||
// function name -> cfg data
|
||||
export type CFGResult = Record<string, CfgDescriptor>;
|
||||
export type AnnotatedCFGResult = Record<string, AnnotatedCfgDescriptor>;
|
||||
@@ -1,14 +1,9 @@
|
||||
#cfg
|
||||
.top-bar.btn-toolbar.bg-light.cfg-toolbar(role="toolbar")
|
||||
.btn-group.btn-group-sm(role="group")
|
||||
select.function-picker
|
||||
.btn-group.btn-group-sm.options(role="group")
|
||||
.button-checkbox
|
||||
button.btn.btn-sm.btn-light.toggle-navigation(type="button" title="Toggle navigation buttons" aria-pressed="false" data-bind="navigation")
|
||||
span Nav
|
||||
input.d-none(type="checkbox")
|
||||
.button-checkbox
|
||||
button.btn.btn-sm.btn-light.toggle-physics(type="button" title="Toggle physics to nodes" aria-pressed="false" data-bind="physics")
|
||||
span Physics
|
||||
input.d-none(type="checkbox")
|
||||
.graph-placeholder
|
||||
select.function-selector
|
||||
.graph-container
|
||||
span.cfg-info
|
||||
.graph
|
||||
svg
|
||||
.block-container
|
||||
|
||||
@@ -61,7 +61,7 @@ mixin newPaneButton(classId, text, title, icon)
|
||||
+newPaneButton("view-gccdump", "GCC Tree/RTL", "Show GCC Tree/RTL dump", "fas fa-tree")
|
||||
+newPaneButton("view-gnatdebugtree", "GNAT Debug Tree", "Show GNAT debug tree", "fas fa-tree")
|
||||
+newPaneButton("view-gnatdebug", "GNAT Debug Expanded Code", "Show GNAT debug expanded code", "fas fa-tree")
|
||||
+newPaneButton("view-cfg", "Graph", "Show graph output", "fas fa-exchange-alt")
|
||||
+newPaneButton("view-cfg", "Control Flow Graph", "Show assembly control flow graphs", "fas fa-exchange-alt")
|
||||
.btn-group.btn-group-sm(role="group")
|
||||
button.btn.btn-sm.btn-light.dropdown-toggle.add-tool(type="button" title="Add tool" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" aria-label="Add tooling to this editor and compiler")
|
||||
span.fas.fa-screwdriver
|
||||
|
||||
Reference in New Issue
Block a user