// Copyright (c) 2017, Marc Poulhiès - Kalray Inc. // 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 $ from 'jquery'; import _ from 'underscore'; import {Container} from 'golden-layout'; import {Hub} from '../hub.js'; import TomSelect from 'tom-select'; import {Toggles} from '../widgets/toggles.js'; import * as monaco from 'monaco-editor'; import {MonacoPane} from './pane.js'; import {MonacoPaneState, PaneState} from './pane.interfaces.js'; import * as monacoConfig from '../monaco-config.js'; import {GccDumpFiltersState, GccDumpViewState, GccDumpViewSelectedPass} from './gccdump-view.interfaces.js'; import {ga} from '../analytics.js'; import {unwrap, assert} from '../assert.js'; import {CompilationResult} from '../compilation/compilation.interfaces.js'; import {CompilerInfo} from '../compiler.interfaces.js'; export class GccDump extends MonacoPane { selectize: TomSelect; uiIsReady: boolean; filters: Toggles; dumpFiltersButtons: JQuery; dumpTreesButton: JQuery; dumpTreesTitle: string; dumpRtlButton: JQuery; dumpRtlTitle: string; dumpIpaButton: JQuery; dumpIpaTitle: string; optionGimpleFeButton: JQuery; optionGimpleFeTitle: string; optionAddressButton: JQuery; optionAddressTitle: string; optionSlimButton: JQuery; optionSlimTitle: string; optionRawButton: JQuery; optionRawTitle: string; optionDetailsButton: JQuery; optionDetailsTitle: string; optionStatsButton: JQuery; optionStatsTitle: string; optionBlocksButton: JQuery; optionBlocksTitle: string; optionVopsButton: JQuery; optionVopsTitle: string; optionLinenoButton: JQuery; optionLinenoTitle: string; optionUidButton: JQuery; optionUidTitle: string; optionAllButton: JQuery; optionAllTitle: string; inhibitPassSelect = false; cursorSelectionThrottledFunction: ((e: any) => void) & _.Cancelable; selectedPass: string | null = null; constructor(hub: Hub, container: Container, state: GccDumpViewState & MonacoPaneState) { super(hub, container, state); if (state.selectedPass && typeof state.selectedPass === 'string') { // To keep URL format stable wrt GccDump, only a string of the form 'r.expand' is stored. // Old links also have the pass number prefixed but this can be ignored. // Create the object that will be used instead of this bare string. const selectedPassRe = /[0-9]*(i|t|r)\.([\w-_]*)/; const passType = { i: 'ipa', r: 'rtl', t: 'tree', }; const match = state.selectedPass.match(selectedPassRe); if (match) { const selectedPassO: GccDumpViewSelectedPass = { filename_suffix: match[1] + '.' + match[2], name: match[2] + ' (' + passType[match[1]] + ')', command_prefix: '-fdump-' + passType[match[1]] + '-' + match[2], // FIXME(dkm): maybe this could be avoided by better typing. selectedPass: null, }; this.eventHub.emit('gccDumpPassSelected', this.compilerInfo.compilerId, selectedPassO, false); } } // until we get our first result from compilation backend with all fields, // disable UI callbacks. this.uiIsReady = false; this.onUiNotReady(); this.eventHub.emit('gccDumpFiltersChanged', this.compilerInfo.compilerId, this.getEffectiveFilters(), false); this.updateButtons(); this.updateState(); // UI is ready, request compilation to get passes list and // current output (if any) this.eventHub.emit('gccDumpUIInit', this.compilerInfo.compilerId); } override initializeCompilerInfo(state: PaneState) { super.initializeCompilerInfo(state); if (!state.id && (state as any)._compilerid) this.compilerInfo.compilerId = (state as any)._compilerid; if (!state.editorid && (state as any)._editorid) this.compilerInfo.editorId = (state as any)._editorid; if (!state.compilerName && (state as any)._compilerName) this.compilerInfo.compilerName = (state as any)._compilerName; if (!state.treeid && (state as any)._treeid) state.treeid = (state as any)._treeid; } override getInitialHTML(): string { return $('#gccdump').html(); } override createEditor(editorRoot: HTMLElement): void { this.editor = monaco.editor.create( editorRoot, monacoConfig.extendConfig({ readOnly: true, glyphMargin: true, lineNumbersMinChars: 3, dropdownParent: 'body', }), ); } override getPrintName() { return 'GCC Tree/RTL Output'; } override getDefaultPaneName() { return 'GCC Tree/RTL Viewer'; } override registerOpeningAnalyticsEvent() { ga.proxy('send', { hitType: 'event', eventCategory: 'OpenViewPane', eventAction: 'GccDump', }); } override registerButtons(state: GccDumpViewState & MonacoPaneState) { super.registerButtons(state); const gccdump_picker = this.domRoot.find('.gccdump-pass-picker').get(0); if (!(gccdump_picker instanceof HTMLSelectElement)) { throw new Error('.gccdump-pass-picker is not an HTMLSelectElement'); } assert(gccdump_picker instanceof HTMLSelectElement); this.selectize = new TomSelect(gccdump_picker, { sortField: undefined, // do not sort valueField: 'name', labelField: 'name', searchField: ['name'], options: [], items: [], plugins: ['input_autogrow'], maxOptions: 500, }); this.filters = new Toggles(this.domRoot.find('.dump-filters'), state as any as Record); this.dumpFiltersButtons = this.domRoot.find('.dump-filters .btn'); this.dumpTreesButton = this.domRoot.find("[data-bind='treeDump']"); this.dumpTreesTitle = this.dumpTreesButton.prop('title'); this.dumpRtlButton = this.domRoot.find("[data-bind='rtlDump']"); this.dumpRtlTitle = this.dumpRtlButton.prop('title'); this.dumpIpaButton = this.domRoot.find("[data-bind='ipaDump']"); this.dumpIpaTitle = this.dumpIpaButton.prop('title'); this.optionGimpleFeButton = this.domRoot.find("[data-bind='gimpleFe']"); this.optionGimpleFeTitle = this.optionGimpleFeButton.prop('title'); this.optionAddressButton = this.domRoot.find("[data-bind='addressOption']"); this.optionAddressTitle = this.optionAddressButton.prop('title'); this.optionSlimButton = this.domRoot.find("[data-bind='slimOption']"); this.optionSlimTitle = this.optionSlimButton.prop('title'); this.optionRawButton = this.domRoot.find("[data-bind='rawOption']"); this.optionRawTitle = this.optionRawButton.prop('title'); this.optionDetailsButton = this.domRoot.find("[data-bind='detailsOption']"); this.optionDetailsTitle = this.optionDetailsButton.prop('title'); this.optionStatsButton = this.domRoot.find("[data-bind='statsOption']"); this.optionStatsTitle = this.optionStatsButton.prop('title'); this.optionBlocksButton = this.domRoot.find("[data-bind='blocksOption']"); this.optionBlocksTitle = this.optionBlocksButton.prop('title'); this.optionVopsButton = this.domRoot.find("[data-bind='vopsOption']"); this.optionVopsTitle = this.optionVopsButton.prop('title'); this.optionLinenoButton = this.domRoot.find("[data-bind='linenoOption']"); this.optionLinenoTitle = this.optionLinenoButton.prop('title'); this.optionUidButton = this.domRoot.find("[data-bind='uidOption']"); this.optionUidTitle = this.optionUidButton.prop('title'); this.optionAllButton = this.domRoot.find("[data-bind='allOption']"); this.optionAllTitle = this.optionAllButton.prop('title'); } override registerCallbacks() { this.filters.on('change', this.onFilterChange.bind(this)); this.selectize.on('change', this.onPassSelect.bind(this)); this.selectize.on('dropdown_open', () => { // Prevent overflowing the window const dropdown = this.selectize.dropdown_content; dropdown.style.maxHeight = `${window.innerHeight - dropdown.getBoundingClientRect().top - 10}px`; }); this.selectize.on('dropdown_close', () => { // scroll back to the selection on the next open if (!this.selectedPass) return; // Make the trip back from the stored selectedPass to the TomSelect option const activeOption = Object.entries(this.selectize.options).find( op => op[1].filename_suffix === this.selectedPass, ); const selectedPassId = unwrap(activeOption)[0]; const option = this.selectize.getOption(selectedPassId); this.selectize.setActiveOption(option); }); this.eventHub.emit('gccDumpViewOpened', this.compilerInfo.compilerId); this.eventHub.emit('requestSettings'); this.container.on('resize', this.resize, this); this.container.on('shown', this.resize, this); this.cursorSelectionThrottledFunction = _.throttle(this.onDidChangeCursorSelection.bind(this), 500); this.editor.onDidChangeCursorSelection(e => { this.cursorSelectionThrottledFunction(e); }); } updateButtons() { const formatButtonTitle = (button, title) => button.prop('title', '[' + (button.hasClass('active') ? 'ON' : 'OFF') + '] ' + title); formatButtonTitle(this.dumpTreesButton, this.dumpTreesTitle); formatButtonTitle(this.dumpRtlButton, this.dumpRtlTitle); formatButtonTitle(this.dumpIpaButton, this.dumpIpaTitle); formatButtonTitle(this.optionGimpleFeButton, this.optionGimpleFeTitle); formatButtonTitle(this.optionAddressButton, this.optionAddressTitle); formatButtonTitle(this.optionSlimButton, this.optionSlimTitle); formatButtonTitle(this.optionRawButton, this.optionRawTitle); formatButtonTitle(this.optionDetailsButton, this.optionDetailsTitle); formatButtonTitle(this.optionStatsButton, this.optionStatsTitle); formatButtonTitle(this.optionBlocksButton, this.optionBlocksTitle); formatButtonTitle(this.optionVopsButton, this.optionVopsTitle); formatButtonTitle(this.optionLinenoButton, this.optionLinenoTitle); formatButtonTitle(this.optionUidButton, this.optionUidTitle); formatButtonTitle(this.optionAllButton, this.optionAllTitle); } // Disable view's menu when invalid compiler has been // selected after view is opened. onUiNotReady() { // disable drop down menu and buttons this.selectize.disable(); this.dumpFiltersButtons.prop('disabled', true); } onUiReady() { // enable drop down menu and buttons this.selectize.enable(); this.dumpFiltersButtons.prop('disabled', false); } onPassSelect(passId: string) { const selectedPass = this.selectize.options[passId] as unknown as GccDumpViewSelectedPass; if (this.inhibitPassSelect !== true) { this.eventHub.emit('gccDumpPassSelected', this.compilerInfo.compilerId, selectedPass, true); } // To keep shared URL compatible, we keep on storing only a string in the // state and stick to the original format. // Previously, we were simply storing the full file suffix (the part after [...]): // [file.c.]123t.expand // We don't have the number now, but we can store the file suffix without this number // (the number is useless and should probably have never been there in the // first place). this.selectedPass = selectedPass.filename_suffix; this.updateState(); } // Called after result from new compilation received // if gccDumpOutput is false, cleans the select menu updatePass(filters, selectize, gccDumpOutput) { const passes = gccDumpOutput ? gccDumpOutput.all : []; // we are changing selectize but don't want any callback to // trigger new compilation this.inhibitPassSelect = true; selectize.clear(true); for (const p of passes) { selectize.addOption(p); } if (gccDumpOutput.selectedPass) selectize.addItem(gccDumpOutput.selectedPass.name, true); else selectize.clear(true); this.eventHub.emit('gccDumpPassSelected', this.compilerInfo.compilerId, gccDumpOutput.selectedPass, false); this.inhibitPassSelect = false; } override onCompileResult(id: number, compiler: CompilerInfo, result: CompilationResult) { if (this.compilerInfo.compilerId !== id) return; const model = this.editor.getModel(); if (model) { if (result.gccDumpOutput && result.gccDumpOutput.syntaxHighlight) { monaco.editor.setModelLanguage(model, 'gccdump-rtl-gimple'); } else { monaco.editor.setModelLanguage(model, 'plaintext'); } } if (compiler.supportsGccDump && result.gccDumpOutput) { const currOutput = result.gccDumpOutput.currentPassOutput; // if result contains empty selected pass, probably means // we requested an invalid/outdated pass. if (!result.gccDumpOutput.selectedPass) { this.selectize.clear(true); this.selectedPass = null; } this.updatePass(this.filters, this.selectize, result.gccDumpOutput); this.showGccDumpResults(currOutput); // enable UI on first successful compilation or after an invalid compiler selection (eg. clang) if (!this.uiIsReady) { this.uiIsReady = true; this.onUiReady(); } } else { this.selectize.clear(true); this.selectedPass = null; this.updatePass(this.filters, this.selectize, false); this.uiIsReady = false; this.onUiNotReady(); if (!compiler.supportsGccDump) { this.showGccDumpResults(''); } else { this.showGccDumpResults(''); } } this.updateState(); } showGccDumpResults(results) { this.editor.setValue(results); if (!this.isAwaitingInitialResults) { if (this.selection) { this.editor.setSelection(this.selection); this.editor.revealLinesInCenter(this.selection.startLineNumber, this.selection.endLineNumber); } this.isAwaitingInitialResults = true; } } override onCompiler(compilerId: number, compiler, options: unknown, editorId: number, treeId: number) { if (compilerId === this.compilerInfo.compilerId) { this.compilerInfo.compilerName = compiler ? compiler.name : ''; this.compilerInfo.editorId = editorId; this.compilerInfo.treeId = treeId; this.updateTitle(); // TODO(jeremy-rifkin): Panes like ast-view handle the case here where the compiler doesn't support // the view } } override onCompilerClose(id) { if (id === this.compilerInfo.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(function (self) { self.container.close(); }, this); } } getEffectiveFilters(): GccDumpFiltersState { // This cast only works if gccdump.pug and gccdump-view.interfaces are // kept synchronized. See comment in gccdump-view.interfaces.ts. return this.filters.get() as unknown as GccDumpFiltersState; } onFilterChange() { this.updateState(); this.updateButtons(); if (this.inhibitPassSelect !== true) { this.eventHub.emit('gccDumpFiltersChanged', this.compilerInfo.compilerId, this.getEffectiveFilters(), true); } } override getCurrentState() { const parent = super.getCurrentState(); const filters = this.getEffectiveFilters(); // TODO: Validate somehow? const state: MonacoPaneState & GccDumpViewState = { // filters needs to come first, the entire state is given to the toggles and we don't want to override // properties such as selectedPass with obsolete values ...filters, selectedPass: this.selectedPass, // See FIXME(dkm) comment in gccdump-view.interfaces.ts. filename_suffix: this.selectedPass, name: null, command_prefix: null, ...parent, }; // TODO(jeremy-rifkin) return state as any; } override close() { this.eventHub.unsubscribe(); this.eventHub.emit('gccDumpViewClosed', this.compilerInfo.compilerId); this.editor.dispose(); } }