diff --git a/cypress/integration/frontend-testing.js b/cypress/integration/frontend-testing.js index 83b02ec2912ab042159badeac5657c503e0e0996..20fb852d8ec736f6c4f77a6dc9a896e27d3d01d9 100644 --- a/cypress/integration/frontend-testing.js +++ b/cypress/integration/frontend-testing.js @@ -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', () => { diff --git a/lib/cfg.js b/lib/cfg.js index dbaefe2b3fcc5d4e659e53f1d82679529a121e7b..64d09b26bb50b6173b0b4b7fe13a85cd981dce75 100644 --- a/lib/cfg.js +++ b/lib/cfg.js @@ -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'); } diff --git a/package-lock.json b/package-lock.json index 7ce65f5fe5d760c0018c0036da7ed0f461b0eeeb..e0206614356c466fb27dd5d736a60c9ddac50f88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 83b674025d0b5d7ecaea543dcabef0eb690deef7..6fa21d4a23961f4cd82002e8e019953eb9b87ccc 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/static/components.interfaces.ts b/static/components.interfaces.ts index faa0a9988e74fe948310efe7f725e057956807db..a69dfed1a768a258343195df27a5a03c5dbbfe9d 100644 --- a/static/components.interfaces.ts +++ b/static/components.interfaces.ts @@ -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 = { diff --git a/static/components.ts b/static/components.ts index d73cea64b2cf418f0e8417f96ea669617648a45c..85cabb9da4d99f131e3edc1fce643ff2d78b1eeb 100644 --- a/static/components.ts +++ b/static/components.ts @@ -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, diff --git a/static/graph-layout-core.ts b/static/graph-layout-core.ts new file mode 100644 index 0000000000000000000000000000000000000000..a9b44f83c649300ef2f5f391dcdec7874645551d --- /dev/null +++ b/static/graph-layout-core.ts @@ -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; + } +} diff --git a/static/noscript.scss b/static/noscript.scss index 81aa4ffab5f26dc350a3fdcaef374c4e20d0838c..b035d8b505f73722dfe2f4e63b4afb098f9dd0d7 100644 --- a/static/noscript.scss +++ b/static/noscript.scss @@ -1,5 +1,4 @@ @import '~@fortawesome/fontawesome-free/css/all.min.css'; -@import '~vis-network/styles/vis-network.css'; html body { overflow: auto; diff --git a/static/panes/cfg-view.interfaces.ts b/static/panes/cfg-view.interfaces.ts index 5683354f2f0e22b8e17f3c4a356186e5a50d29e3..f9a8c9f9d9037fcd27883c2b37633acbe5ee4204 100644 --- a/static/panes/cfg-view.interfaces.ts +++ b/static/panes/cfg-view.interfaces.ts @@ -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; } + +*/ diff --git a/static/panes/cfg-view.ts b/static/panes/cfg-view.ts index 406fb2eab9d38ca46c5573219c9b5a28905615d8..86c016ee9c2ce46fbddaf75917d254e71ba72808 100644 --- a/static/panes/cfg-view.ts +++ b/static/panes/cfg-view.ts @@ -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 {Pane} from './pane'; +import * as monaco from 'monaco-editor'; import $ from 'jquery'; -import * as vis from 'vis-network'; import _ from 'underscore'; + +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 {Toggles} from '../widgets/toggles'; +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'; -import {Container} from 'golden-layout'; -import {CfgOptions, CfgState} from './cfg-view.interfaces'; -import {Hub} from '../hub'; -import {Pane} from './pane'; - -interface NodeInfo { - edges: string[]; - dagEdges: string[]; - index: string; - id: number; - level: number; - state: number; - inCount: number; -} - -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>; - - constructor(hub: Hub, container: Container, state: CfgState) { - 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: [], - }; +const ColorTable = { + red: '#FE5D5D', + green: '#76E381', + blue: '#65B7F6', + grey: '#c5c5c5', +}; - this.savedPos = state.pos; - this.savedScale = state.scale; - this.needsMove = this.savedPos && this.savedScale; +type Coordinate = { + x: number; + y: number; +}; - this.currentFunc = state.selectedFn || ''; - this.functions = {}; +const DZOOM = 0.1; +const MINZOOM = 0.1; - 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'], +export class Cfg extends Pane<CfgState> { + 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 & 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.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 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 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 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); + 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>', + }); } - }); - if (data.nodes) { - for (const node of nodes) { - data.nodes[node.index]['level'] = node.level; + for (const fn of keys) { + this.functionSelector.addOption({ + title: fn, + value: fn, + }); } - } - - 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)); + if (keys.length > 0) { + if (selectedFunction === '' || !(selectedFunction !== null && selectedFunction in cfg)) { + selectedFunction = keys[0]; } + this.functionSelector.setValue(selectedFunction, true); + this.state.selectedFunction = selectedFunction; } else { - edge.physics = false; + // 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); } } - 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; + 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; } } - 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); + 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); } - override close() { - this.eventHub.unsubscribe(); - this.eventHub.emit('cfgViewClosed', this.compilerInfo.compilerId); - this.cfgVisualiser.destroy(); + 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'; + } } - getEffectiveOptions() { - return this.toggles.get(); + // 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 getCurrentState(): CfgState { - return { - ...super.getCurrentState(), - selectedFn: this.currentFunc, - pos: this.cfgVisualiser.getViewPosition(), - scale: this.cfgVisualiser.getScale(), - options: this.getEffectiveOptions(), - }; + override resize() { + _.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`; + }); } - adaptStructure(names: string[]) { - return names.map(name => { - return {name}; - }); + override close(): void { + this.eventHub.unsubscribe(); + this.eventHub.emit('cfgViewClosed', this.compilerInfo.compilerId); } } diff --git a/static/panes/compiler.ts b/static/panes/compiler.ts index 2389f9deaefcd47c4a03549a0431d322cfa9be11..cfca3cef3a260762556951c1c6427cffc82379d9 100644 --- a/static/panes/compiler.ts +++ b/static/panes/compiler.ts @@ -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; diff --git a/static/styles/explorer.scss b/static/styles/explorer.scss index d286c3ec935608057436b22ee49d1f5ab4207fb4..7d075e99d010221aac5efbaabf21dbb0008a6966 100644 --- a/static/styles/explorer.scss +++ b/static/styles/explorer.scss @@ -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 { diff --git a/static/styles/themes/dark-theme.scss b/static/styles/themes/dark-theme.scss index 5bfc218ec3d033144afcae231cf43e0a3d9853e1..a6c4a51ce2437eb74c9ecc6d94f8a7245edbcb28 100644 --- a/static/styles/themes/dark-theme.scss +++ b/static/styles/themes/dark-theme.scss @@ -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 { diff --git a/static/styles/themes/default-theme.scss b/static/styles/themes/default-theme.scss index 70ab1a445fff38536956a9a0465f01ceb6757444..44e3ad3abe157f41b6dc224ffa9dce505fb9198a 100644 --- a/static/styles/themes/default-theme.scss +++ b/static/styles/themes/default-theme.scss @@ -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 { diff --git a/test/cfg-cases/cfg-gcc.if-else.json b/test/cfg-cases/cfg-gcc.if-else.json index f8b61a129a200c79d99f9b69a42e933a5682b1fc..662bc3066b342be1c66a3ec3c3093de0e439e968 100644 --- a/test/cfg-cases/cfg-gcc.if-else.json +++ b/test/cfg-cases/cfg-gcc.if-else.json @@ -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" } diff --git a/test/cfg-cases/cfg-gcc.loop.json b/test/cfg-cases/cfg-gcc.loop.json index 25e06b7bbb02a26502321ffbc038896ca13cae85..ca3b1dc71d72c744fdb5b97c56aa0790d3887b26 100644 --- a/test/cfg-cases/cfg-gcc.loop.json +++ b/test/cfg-cases/cfg-gcc.loop.json @@ -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" } diff --git a/test/cfg-cases/cfg-gcc.single-block.json b/test/cfg-cases/cfg-gcc.single-block.json index 41d33f20736765f47db4b00996e92dee1a315c43..de7c2c76ba51269846430a1e106fa8e1b308f5fd 100644 --- a/test/cfg-cases/cfg-gcc.single-block.json +++ b/test/cfg-cases/cfg-gcc.single-block.json @@ -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" } diff --git a/types/compilation/cfg.interfaces.ts b/types/compilation/cfg.interfaces.ts new file mode 100644 index 0000000000000000000000000000000000000000..54e034e370bd49304f214b2a4628c9e3d232741b --- /dev/null +++ b/types/compilation/cfg.interfaces.ts @@ -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>; diff --git a/views/templates/panes/cfg.pug b/views/templates/panes/cfg.pug index 6ff3066cb346d424e236d86a64a6406c61c1c5ad..1a6446282da8b7d814ce4bb4152552596f2f9a7e 100644 --- a/views/templates/panes/cfg.pug +++ b/views/templates/panes/cfg.pug @@ -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 diff --git a/views/templates/panes/compiler.pug b/views/templates/panes/compiler.pug index 1612fdf5c99268268204a91138aec6386f759c95..45ec083a6164c58e4bceceeabfc7f297fb328e13 100644 --- a/views/templates/panes/compiler.pug +++ b/views/templates/panes/compiler.pug @@ -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