mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2026-07-16 17:26:58 -04:00
Following the discussion at #7870: `-fdump-passes` reports passes enabled dynamically (e.g. loop2_unroll via `#pragma GCC unroll`) as OFF, so they were missing from the pass dropdown although they were active. Fixed by building the pass list from the dump files GCC actually wrote instead. While reworking this: - Read all passes into a `passDumps` map shipped with the result, so switching passes doesn't trigger a compile. - Trim each dump to functions defined in the user's source, dropping header/library functions; strip forced -lineno annotations from GIMPLE (tree/IPA) dumps when Line Numbers is off, leaving RTL untouched. - Exclude passes with empty output - Add a GccDumpOutput type, backend unit tests and a cypress test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -24,15 +24,17 @@
|
||||
|
||||
import {
|
||||
assertNoConsoleOutput,
|
||||
findPane,
|
||||
monacoEditorTextShouldContain,
|
||||
openGccDump,
|
||||
setupAndWaitForCompilation,
|
||||
visitPage,
|
||||
} from '../support/utils';
|
||||
|
||||
// Anchor on the pane's own pass-picker rather than the GoldenLayout tab title: the title query
|
||||
// (`span.lm_title:visible`) proved flaky right after a TomSelect dropdown interaction, whereas the
|
||||
// `.gccdump-pass-picker` element is unique to this pane and always present in its content area.
|
||||
function gccDumpPane() {
|
||||
return findPane('GCC Tree');
|
||||
return cy.get('.gccdump-pass-picker', {timeout: 10000}).closest('.lm_content');
|
||||
}
|
||||
|
||||
beforeEach(visitPage);
|
||||
@@ -61,7 +63,41 @@ describe('GCC Tree/RTL dump', () => {
|
||||
setupAndWaitForCompilation();
|
||||
openGccDump();
|
||||
cy.get('.gccdump-pass-picker + .ts-wrapper .ts-control', {timeout: 10000}).should('be.visible').click();
|
||||
cy.get('.ts-dropdown .option:visible', {timeout: 10000}).first().click();
|
||||
cy.get('.ts-dropdown .option:visible', {timeout: 10000}).should('have.length.greaterThan', 0);
|
||||
// Pick a tree pass specifically: the first option is now an IPA summary (cgraph), whereas
|
||||
// this test is about the GIMPLE/tree body, which is what shows the `square` function.
|
||||
cy.contains('.ts-dropdown .option:visible', '(tree)').click();
|
||||
monacoEditorTextShouldContain(gccDumpPane().find('.monaco-editor'), 'square');
|
||||
});
|
||||
|
||||
it('should switch passes without triggering a recompile', () => {
|
||||
// All pass dumps are shipped on the initial compile, so selecting a different pass is a
|
||||
// client-side lookup. Count compile requests and assert switching passes adds none.
|
||||
let compileCount = 0;
|
||||
cy.intercept('POST', '**/api/compiler/**/compile', req => {
|
||||
compileCount++;
|
||||
req.continue();
|
||||
}).as('compile');
|
||||
|
||||
setupAndWaitForCompilation();
|
||||
openGccDump();
|
||||
|
||||
// Wait for the picker to populate from the initial compile, then pick a first pass.
|
||||
cy.get('.gccdump-pass-picker + .ts-wrapper .ts-control', {timeout: 10000}).should('be.visible').click();
|
||||
cy.get('.ts-dropdown .option:visible', {timeout: 10000}).should('have.length.greaterThan', 1);
|
||||
cy.get('.ts-dropdown .option:visible').first().click();
|
||||
|
||||
// Once settled, switch to a different pass and assert no new compile fired. (Content is
|
||||
// covered by the test above; here we only care that switching is a client-side lookup.)
|
||||
cy.then(() => {
|
||||
const before = compileCount;
|
||||
cy.get('.gccdump-pass-picker + .ts-wrapper .ts-control').click();
|
||||
cy.get('.ts-dropdown .option:visible').eq(1).click();
|
||||
// Give any (unwanted) recompile a chance to be issued before asserting.
|
||||
cy.wait(1500);
|
||||
cy.then(() => {
|
||||
expect(compileCount, 'compile requests during pass switch').to.equal(before);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -741,7 +741,7 @@ export class BaseCompiler {
|
||||
}
|
||||
|
||||
getGccDumpOptions(gccDumpOptions: Record<string, any>, outputFilename: string) {
|
||||
const addOpts = ['-fdump-passes'];
|
||||
const addOpts: string[] = [];
|
||||
|
||||
// Build dump options to append to the end of the -fdump command-line flag.
|
||||
// GCC accepts these options as a list of '-' separated names that may
|
||||
@@ -785,21 +785,28 @@ export class BaseCompiler {
|
||||
}
|
||||
|
||||
// If we want to remove the passes that won't produce anything from the
|
||||
// drop down menu, we need to ask for all dump files and see what's
|
||||
// really created. This is currently only possible with regular GCC, not
|
||||
// for compilers that us libgccjit. The later can't easily move dump
|
||||
// files outside of the tempdir created on the fly.
|
||||
// drop down menu, we dump every invoked pass to its own file and then
|
||||
// enumerate what was actually produced (see processGccDumpOutput). This is
|
||||
// only possible with regular GCC, not for compilers that use libgccjit. The
|
||||
// latter can't easily move dump files outside of the tempdir created on the
|
||||
// fly, so they keep relying on the -fdump-passes listing parsed from stderr.
|
||||
if (this.compiler.removeEmptyGccDump) {
|
||||
// Force '-lineno' on: processGccDumpOutput uses the per-statement
|
||||
// [file:line] annotations to keep only functions defined in the user's
|
||||
// source (header/library functions are trimmed out). If the user turned
|
||||
// lineno off, the annotations are stripped again before the dump is shown.
|
||||
const allFlags = flags.includes('-lineno') ? flags : flags + '-lineno';
|
||||
if (gccDumpOptions.treeDump !== false) {
|
||||
addOpts.push('-fdump-tree-all' + flags);
|
||||
addOpts.push('-fdump-tree-all' + allFlags);
|
||||
}
|
||||
if (gccDumpOptions.rtlDump !== false) {
|
||||
addOpts.push('-fdump-rtl-all' + flags);
|
||||
addOpts.push('-fdump-rtl-all' + allFlags);
|
||||
}
|
||||
if (gccDumpOptions.ipaDump !== false) {
|
||||
addOpts.push('-fdump-ipa-all' + flags);
|
||||
addOpts.push('-fdump-ipa-all' + allFlags);
|
||||
}
|
||||
} else {
|
||||
addOpts.push('-fdump-passes');
|
||||
// If not dumping everything, create a specific command like
|
||||
// -fdump-tree-fixup_cfg1-some-flags=somefilename
|
||||
if (gccDumpOptions.pass) {
|
||||
@@ -1924,6 +1931,78 @@ export class BaseCompiler {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a GCC dump file name (`<inputbase>.<NNN><letter>.<passname>`, e.g.
|
||||
* `example.cpp.255r.expand`) into the same descriptor shape as fromInternalGccDumpName.
|
||||
* Returns null for files that aren't pass dumps (the .s output, .o, the source, ...).
|
||||
* `nnn` is GCC's global pass-execution counter, used to order the drop-down.
|
||||
*/
|
||||
passObjectFromDumpFilename(filename: string) {
|
||||
// letter: t=tree, r=rtl, i=ipa. passname has hyphens/digits/underscores but no dots.
|
||||
const match = filename.match(/\.(\d+)([tri])\.([\w-]+)$/);
|
||||
if (!match) return null;
|
||||
|
||||
const category = {t: 'tree', r: 'rtl', i: 'ipa'}[match[2] as 't' | 'r' | 'i'];
|
||||
const passName = match[3];
|
||||
return {
|
||||
nnn: Number.parseInt(match[1], 10),
|
||||
// Keep these three fields byte-identical to fromInternalGccDumpName: the frontend
|
||||
// keys TomSelect items by `name` and rebuilds the others from the stored suffix.
|
||||
filename_suffix: `${match[2]}.${passName}`,
|
||||
name: `${passName} (${category})`,
|
||||
command_prefix: `-fdump-${category}-${passName}`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Trim a GCC dump so it only contains functions defined in the user's source file,
|
||||
* dropping functions that originate from included headers (#7870-era behaviour: this is
|
||||
* the "defined in the actual source, not included headers" filter).
|
||||
*
|
||||
* Functions are delimited by `;; Function <sym> (...)` headers. We classify a block by
|
||||
* substring rather than by parsing location syntax: tree/GIMPLE dumps annotate with
|
||||
* `[file:line]`, but RTL dumps use `"file":line` plus unrelated brackets such as `[orig:N]`,
|
||||
* so a syntax-based "first location" match is fragile and was wrongly emptying RTL passes.
|
||||
* A block is treated as a header function (and dropped) only when it references a system
|
||||
* include path but never the user's source file; everything else — including blocks with no
|
||||
* recognisable location at all — is kept. Dumps without any `;; Function` markers (IPA
|
||||
* summaries such as cgraph) are returned whole.
|
||||
*
|
||||
* `isRtlDump` selects the lineno handling: `-lineno` adds `[file:line]` prefixes to every
|
||||
* GIMPLE statement, which appears in tree dumps AND in IPA passes that print GIMPLE bodies
|
||||
* (icf, inline, sra, ...). So when the user disabled lineno but we forced it on for origin
|
||||
* detection, those prefixes are stripped from all non-RTL dumps. RTL dumps are left untouched
|
||||
* because they use different bracket syntax (`[orig:N]`, nested `[N [file:line]]`) that the
|
||||
* strip regex would corrupt, and their locations come from -g rather than -lineno anyway.
|
||||
*/
|
||||
trimGccDumpHeaderFunctions(
|
||||
content: string,
|
||||
sourceBasename: string,
|
||||
keepLineno: boolean,
|
||||
isRtlDump: boolean,
|
||||
): string {
|
||||
// Splitting on a lookahead keeps each `;; Function` header with its block and leaves the
|
||||
// preamble (text before the first function) as the first piece, so join('') is lossless.
|
||||
const pieces = content.split(/(?=^;; Function )/m);
|
||||
|
||||
const isHeaderFunction = (piece: string) =>
|
||||
!piece.includes(sourceBasename) && /\/usr\/|\/opt\/|\/include\//.test(piece);
|
||||
|
||||
const kept =
|
||||
pieces.length <= 1
|
||||
? pieces // no function markers (e.g. IPA summary dump): keep whole
|
||||
: pieces.filter((piece, index) => index === 0 || !isHeaderFunction(piece));
|
||||
|
||||
let trimmed = kept.join('');
|
||||
if (!keepLineno && !isRtlDump) {
|
||||
// Approximate the no-lineno output by removing the forced [file:line(:col)] prefixes
|
||||
// from GIMPLE (tree + IPA) dumps. RTL is excluded so its [orig:N]/nested brackets are
|
||||
// never touched.
|
||||
trimmed = trimmed.replace(/\[[^[\]\n]*?:\d+(?::\d+)?(?: discrim \d+)?\] ?/g, '');
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
async checkOutputFileAndDoPostProcess(
|
||||
asmResult: CompilationResult,
|
||||
outputFilename: string,
|
||||
@@ -3592,6 +3671,7 @@ export class BaseCompiler {
|
||||
selectedPass: null,
|
||||
currentPassOutput: 'Nothing selected for dump:\nselect at least one of Tree/IPA/RTL filter',
|
||||
syntaxHighlight: false,
|
||||
passDumps: {} as Record<string, string>,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3600,7 +3680,79 @@ export class BaseCompiler {
|
||||
selectedPass: opts.pass ?? null,
|
||||
currentPassOutput: '<No pass selected>',
|
||||
syntaxHighlight: false,
|
||||
passDumps: {} as Record<string, string>,
|
||||
};
|
||||
|
||||
const notDumpedMessage = (passName: string | null) =>
|
||||
`Pass '${passName}' was requested
|
||||
but nothing was dumped. Possible causes are:
|
||||
- pass is not valid in this (maybe you changed the compiler options);
|
||||
- pass is valid but did not emit anything (eg. it was not executed).`;
|
||||
|
||||
if (removeEmptyPasses) {
|
||||
// Enumerate the dump files GCC actually produced (every invoked pass, including
|
||||
// passes turned on dynamically by pragmas -- see #7870) and read them all up front,
|
||||
// so that switching passes in the UI is a client-side lookup with no recompile.
|
||||
const selectedCategories = new Set<string>();
|
||||
if (opts.treeDump) selectedCategories.add('tree');
|
||||
if (opts.ipaDump) selectedCategories.add('ipa');
|
||||
if (opts.rtlDump) selectedCategories.add('rtl');
|
||||
const categoryByLetter: Record<string, string> = {t: 'tree', r: 'rtl', i: 'ipa'};
|
||||
|
||||
const sourceBasename = path.basename(result.inputFilename);
|
||||
// We force -lineno on for origin detection; only KEEP its annotations in the shown
|
||||
// dump when the user explicitly enabled the Line Numbers option. Anything else
|
||||
// (off, or unset) strips them, matching the option's default-off behaviour.
|
||||
const keepLineno = opts.dumpFlags?.lineno === true;
|
||||
|
||||
const candidates: {
|
||||
filename: string;
|
||||
pass: {nnn: number; filename_suffix: string; name: string; command_prefix: string};
|
||||
}[] = [];
|
||||
for (const filename of await fs.readdir(rootDir)) {
|
||||
const pass = this.passObjectFromDumpFilename(filename);
|
||||
if (!pass) continue;
|
||||
if (!selectedCategories.has(categoryByLetter[pass.filename_suffix[0]])) continue;
|
||||
candidates.push({filename, pass});
|
||||
}
|
||||
// Order the drop-down by GCC's global pass-execution counter (stable secondary by name).
|
||||
candidates.sort((a, b) => a.pass.nnn - b.pass.nnn || a.pass.name.localeCompare(b.pass.name));
|
||||
|
||||
for (const {filename, pass} of candidates) {
|
||||
const raw = await utils.tryReadTextFile(path.join(rootDir, filename));
|
||||
const content = raw
|
||||
? this.trimGccDumpHeaderFunctions(raw, sourceBasename, keepLineno, pass.filename_suffix[0] === 'r')
|
||||
: '';
|
||||
// Skip passes that produced nothing for this source (e.g. an empty ipa-clones
|
||||
// file, or a pass whose only output was header functions we trimmed away). This
|
||||
// is the real "remove empty GCC dumps" behaviour: keep the drop-down to passes
|
||||
// that actually have content, so selecting one never yields "nothing was dumped".
|
||||
if (/^\s*$/.test(content)) continue;
|
||||
const {nnn, ...descriptor} = pass;
|
||||
output.all.push(descriptor);
|
||||
output.passDumps[descriptor.filename_suffix] = content;
|
||||
}
|
||||
|
||||
const selectedSuffix = opts.pass?.filename_suffix ?? null;
|
||||
if (selectedSuffix && selectedSuffix in output.passDumps) {
|
||||
const content = output.passDumps[selectedSuffix];
|
||||
if (/^\s*$/.test(content)) {
|
||||
output.currentPassOutput = notDumpedMessage(opts.pass?.name ?? null);
|
||||
} else {
|
||||
output.currentPassOutput = content;
|
||||
output.syntaxHighlight = true;
|
||||
}
|
||||
} else {
|
||||
// The requested pass produced nothing (e.g. disabled by a filter change): clear
|
||||
// the selection so the frontend resets its drop-down.
|
||||
output.selectedPass = null;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
// Legacy path for libgccjit-based compilers (removeEmptyGccDump === false): these cannot
|
||||
// relocate dump files, so we can only enumerate passes from the -fdump-passes stderr
|
||||
// listing and dump the single selected pass to a known file via -fdump-<cat>-<pass>=FILE.
|
||||
const treeDumpsNotInPasses: any[] = [];
|
||||
|
||||
const selectedPasses: string[] = [];
|
||||
@@ -3634,10 +3786,7 @@ export class BaseCompiler {
|
||||
if (opts.ipaDump) selectedPasses.push('ipa');
|
||||
if (opts.rtlDump) selectedPasses.push('rtl');
|
||||
|
||||
// Defaults to a custom file derived from output file name. Works when
|
||||
// using the -fdump-tree-foo=FILE variant (!removeEmptyPasses).
|
||||
// Will be overriden later if not.
|
||||
let dumpFileName = this.getGccDumpFileName(outputFilename);
|
||||
const dumpFileName = this.getGccDumpFileName(outputFilename);
|
||||
let passFound = false;
|
||||
|
||||
const filtered_stderr: any[] = [];
|
||||
@@ -3653,17 +3802,6 @@ export class BaseCompiler {
|
||||
for (const [obj, selectizeObject] of dumpPassesLines) {
|
||||
if (selectizeObject) {
|
||||
if (opts.pass && opts.pass.name === selectizeObject.name) passFound = true;
|
||||
|
||||
if (removeEmptyPasses) {
|
||||
const f = (await fs.readdir(rootDir)).filter(fn => fn.endsWith(selectizeObject.filename_suffix));
|
||||
|
||||
// pass is enabled, but the dump hasn't produced anything:
|
||||
// don't add it to the drop down menu
|
||||
if (f.length === 0) continue;
|
||||
|
||||
if (opts.pass && opts.pass.name === selectizeObject.name) dumpFileName = path.join(rootDir, f[0]);
|
||||
}
|
||||
|
||||
output.all.push(selectizeObject);
|
||||
}
|
||||
|
||||
@@ -3684,10 +3822,7 @@ export class BaseCompiler {
|
||||
// pass is still requested.
|
||||
|
||||
if (/^\s*$/.test(output.currentPassOutput)) {
|
||||
output.currentPassOutput = `Pass '${opts.pass.name}' was requested
|
||||
but nothing was dumped. Possible causes are:
|
||||
- pass is not valid in this (maybe you changed the compiler options);
|
||||
- pass is valid but did not emit anything (eg. it was not executed).`;
|
||||
output.currentPassOutput = notDumpedMessage(opts.pass.name);
|
||||
} else {
|
||||
output.syntaxHighlight = true;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
import GoldenLayout from 'golden-layout';
|
||||
|
||||
import {GccDumpOutput} from '../types/compilation/compilation.interfaces.js';
|
||||
import {ConfiguredOverrides} from '../types/compilation/compiler-overrides.interfaces.js';
|
||||
import {ConfiguredRuntimeTools} from '../types/execution/execution.interfaces.js';
|
||||
import {ParseFiltersAndOutputOptions} from '../types/features/filters.interfaces.js';
|
||||
@@ -71,7 +72,6 @@ import {
|
||||
TREE_COMPONENT_NAME,
|
||||
YUL_VIEW_COMPONENT_NAME,
|
||||
} from './components.interfaces.js';
|
||||
import {GccDumpViewState} from './panes/gccdump-view.interfaces.js';
|
||||
import {SentryCapture} from './sentry.js';
|
||||
|
||||
/** Get an empty compiler component. */
|
||||
@@ -495,7 +495,7 @@ export function getGccDumpViewWith(
|
||||
compilerName: string,
|
||||
editorid: number,
|
||||
treeid: number,
|
||||
gccDumpOutput: GccDumpViewState,
|
||||
gccDumpOutput: GccDumpOutput | undefined,
|
||||
): ComponentConfig<typeof GCC_DUMP_VIEW_COMPONENT_NAME> {
|
||||
return {
|
||||
type: 'component',
|
||||
|
||||
@@ -30,7 +30,7 @@ import TomSelect from 'tom-select';
|
||||
import _ from 'underscore';
|
||||
|
||||
import {assert, unwrap} from '../../shared/assert.js';
|
||||
import {CompilationResult} from '../../types/compilation/compilation.interfaces.js';
|
||||
import {CompilationResult, GccDumpOutput} from '../../types/compilation/compilation.interfaces.js';
|
||||
import {CompilerInfo} from '../../types/compiler.interfaces.js';
|
||||
import {Hub} from '../hub.js';
|
||||
import * as monacoConfig from '../monaco-config.js';
|
||||
@@ -77,6 +77,10 @@ export class GccDump extends MonacoPane<monaco.editor.IStandaloneCodeEditor, Gcc
|
||||
inhibitPassSelect = false;
|
||||
cursorSelectionThrottledFunction: ((e: any) => void) & _.Cancelable;
|
||||
selectedPass: string | null = null;
|
||||
// Maps a pass's filename_suffix to its dump content, sent in full on each compile by
|
||||
// regular-GCC backends. When populated, switching passes is a client-side lookup with no
|
||||
// recompile. Empty for libgccjit compilers, which fall back to recompiling per selection.
|
||||
passDumps: Record<string, string> = {};
|
||||
|
||||
constructor(hub: Hub, container: Container, state: GccDumpViewState & MonacoPaneState) {
|
||||
super(hub, container, state);
|
||||
@@ -171,7 +175,7 @@ export class GccDump extends MonacoPane<monaco.editor.IStandaloneCodeEditor, Gcc
|
||||
options: [],
|
||||
items: [],
|
||||
plugins: ['input_autogrow'],
|
||||
maxOptions: 500,
|
||||
maxOptions: 1000,
|
||||
});
|
||||
|
||||
this.filters = new Toggles(this.domRoot.find('.dump-filters'), state as any as Record<string, boolean>);
|
||||
@@ -313,7 +317,16 @@ export class GccDump extends MonacoPane<monaco.editor.IStandaloneCodeEditor, Gcc
|
||||
}
|
||||
|
||||
if (this.inhibitPassSelect !== true) {
|
||||
this.eventHub.emit('gccDumpPassSelected', this.compilerInfo.compilerId, selectedPass, true);
|
||||
const suffix = selectedPass.filename_suffix;
|
||||
if (suffix !== null && suffix in this.passDumps) {
|
||||
// All pass dumps were shipped with the last compile: switch the view
|
||||
// client-side, no recompile needed.
|
||||
this.showCachedPass(suffix, selectedPass.name);
|
||||
this.eventHub.emit('gccDumpPassSelected', this.compilerInfo.compilerId, selectedPass, false);
|
||||
} else {
|
||||
// No cached dumps (libgccjit compilers): recompile to fetch this single pass.
|
||||
this.eventHub.emit('gccDumpPassSelected', this.compilerInfo.compilerId, selectedPass, true);
|
||||
}
|
||||
}
|
||||
|
||||
// To keep shared URL compatible, we keep on storing only a string in the
|
||||
@@ -328,9 +341,27 @@ export class GccDump extends MonacoPane<monaco.editor.IStandaloneCodeEditor, Gcc
|
||||
this.updateState();
|
||||
}
|
||||
|
||||
notDumpedMessage(passName: string | null): string {
|
||||
return `Pass '${passName}' was requested
|
||||
but nothing was dumped. Possible causes are:
|
||||
- pass is not valid in this (maybe you changed the compiler options);
|
||||
- pass is valid but did not emit anything (eg. it was not executed).`;
|
||||
}
|
||||
|
||||
// Display a pass whose dump content is already cached in this.passDumps (no recompile).
|
||||
showCachedPass(suffix: string, passName: string | null) {
|
||||
const content = this.passDumps[suffix];
|
||||
const hasContent = !/^\s*$/.test(content);
|
||||
const model = this.editor.getModel();
|
||||
if (model) {
|
||||
monaco.editor.setModelLanguage(model, hasContent ? 'gccdump-rtl-gimple' : 'plaintext');
|
||||
}
|
||||
this.showGccDumpResults(hasContent ? content : this.notDumpedMessage(passName));
|
||||
}
|
||||
|
||||
// Called after result from new compilation received
|
||||
// if gccDumpOutput is false, cleans the select menu
|
||||
updatePass(filters: Toggles, selectize: TomSelect, gccDumpOutput) {
|
||||
updatePass(filters: Toggles, selectize: TomSelect, gccDumpOutput: GccDumpOutput | null) {
|
||||
const passes = gccDumpOutput ? gccDumpOutput.all : [];
|
||||
|
||||
// we are changing selectize but don't want any callback to
|
||||
@@ -338,17 +369,25 @@ export class GccDump extends MonacoPane<monaco.editor.IStandaloneCodeEditor, Gcc
|
||||
this.inhibitPassSelect = true;
|
||||
|
||||
selectize.clear(true);
|
||||
selectize.clearOptions();
|
||||
// Pass `() => false` so EVERY option is dropped: TomSelect's default clearOptions filter
|
||||
// keeps options belonging to the currently-selected item, which would otherwise leave a
|
||||
// stale pass (e.g. a Tree pass after Tree dumps are unchecked) in the drop-down.
|
||||
selectize.clearOptions(() => false);
|
||||
|
||||
for (const p of passes) {
|
||||
selectize.addOption(p);
|
||||
}
|
||||
|
||||
if (gccDumpOutput?.selectedPass) {
|
||||
if (gccDumpOutput?.selectedPass?.name) {
|
||||
selectize.addItem(gccDumpOutput.selectedPass.name, true);
|
||||
this.eventHub.emit('gccDumpPassSelected', this.compilerInfo.compilerId, gccDumpOutput.selectedPass, false);
|
||||
} else selectize.clear(true);
|
||||
|
||||
// Re-render the drop-down from the new option set. clearOptions()/addOption() only mark
|
||||
// the rendered content stale; neither they nor a later open() actually repaint it, so
|
||||
// without this the drop-down can keep showing the previous pass list.
|
||||
selectize.refreshOptions(false);
|
||||
|
||||
this.inhibitPassSelect = false;
|
||||
}
|
||||
|
||||
@@ -366,6 +405,11 @@ export class GccDump extends MonacoPane<monaco.editor.IStandaloneCodeEditor, Gcc
|
||||
if (compiler.supportsGccDump && result.gccDumpOutput) {
|
||||
const currOutput = result.gccDumpOutput.currentPassOutput;
|
||||
|
||||
// Cache every pass's dump so later pass selections are served client-side without
|
||||
// a recompile. Replaced wholesale so stale dumps are never consulted after a
|
||||
// filter-change recompile. Empty for libgccjit compilers (recompile-per-pass).
|
||||
this.passDumps = result.gccDumpOutput.passDumps ?? {};
|
||||
|
||||
// if result contains empty selected pass, probably means
|
||||
// we requested an invalid/outdated pass.
|
||||
if (!result.gccDumpOutput.selectedPass) {
|
||||
@@ -381,6 +425,7 @@ export class GccDump extends MonacoPane<monaco.editor.IStandaloneCodeEditor, Gcc
|
||||
this.onUiReady();
|
||||
}
|
||||
} else {
|
||||
this.passDumps = {};
|
||||
this.selectize.clear(true);
|
||||
this.selectedPass = null;
|
||||
this.updatePass(this.filters, this.selectize, null);
|
||||
|
||||
310
test/gccdump-tests.ts
Normal file
310
test/gccdump-tests.ts
Normal file
@@ -0,0 +1,310 @@
|
||||
// Copyright (c) 2026, 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 fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
import {afterEach, beforeAll, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
import {BaseCompiler} from '../lib/base-compiler.js';
|
||||
import {CompilationEnvironment} from '../lib/compilation-env.js';
|
||||
import * as utils from '../lib/utils.js';
|
||||
import {GccDumpOptions} from '../types/compilation/compilation.interfaces.js';
|
||||
import {CompilerInfo} from '../types/compiler.interfaces.js';
|
||||
import {makeCompilationEnvironment, makeFakeCompilerInfo} from './utils.js';
|
||||
|
||||
const languages = {
|
||||
'c++': {id: 'c++'},
|
||||
} as const;
|
||||
|
||||
describe('GCC dump output processing', () => {
|
||||
let ce: CompilationEnvironment;
|
||||
let compiler: BaseCompiler;
|
||||
|
||||
beforeAll(() => {
|
||||
ce = makeCompilationEnvironment({languages});
|
||||
const info = makeFakeCompilerInfo({
|
||||
exe: '/usr/bin/gcc',
|
||||
lang: 'c++',
|
||||
ldPath: [],
|
||||
supportsGccDump: true,
|
||||
removeEmptyGccDump: true,
|
||||
});
|
||||
compiler = new BaseCompiler(info as CompilerInfo, ce);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('passObjectFromDumpFilename', () => {
|
||||
it('parses tree/rtl/ipa dump file names into descriptors', () => {
|
||||
expect(compiler.passObjectFromDumpFilename('example.cpp.255r.expand')).toEqual({
|
||||
nnn: 255,
|
||||
filename_suffix: 'r.expand',
|
||||
name: 'expand (rtl)',
|
||||
command_prefix: '-fdump-rtl-expand',
|
||||
});
|
||||
expect(compiler.passObjectFromDumpFilename('example.cpp.081i.cp')).toEqual({
|
||||
nnn: 81,
|
||||
filename_suffix: 'i.cp',
|
||||
name: 'cp (ipa)',
|
||||
command_prefix: '-fdump-ipa-cp',
|
||||
});
|
||||
expect(compiler.passObjectFromDumpFilename('example.cpp.005t.original')).toEqual({
|
||||
nnn: 5,
|
||||
filename_suffix: 't.original',
|
||||
name: 'original (tree)',
|
||||
command_prefix: '-fdump-tree-original',
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps hyphenated/numbered pass names intact', () => {
|
||||
expect(compiler.passObjectFromDumpFilename('example.cpp.050t.local-pure-const1')).toEqual({
|
||||
nnn: 50,
|
||||
filename_suffix: 't.local-pure-const1',
|
||||
name: 'local-pure-const1 (tree)',
|
||||
command_prefix: '-fdump-tree-local-pure-const1',
|
||||
});
|
||||
expect(compiler.passObjectFromDumpFilename('example.cpp.274r.loop2_unroll')?.name).toEqual(
|
||||
'loop2_unroll (rtl)',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns null for non-dump files', () => {
|
||||
expect(compiler.passObjectFromDumpFilename('example.cpp.s')).toBeNull();
|
||||
expect(compiler.passObjectFromDumpFilename('example.cpp')).toBeNull();
|
||||
expect(compiler.passObjectFromDumpFilename('example.cpp.o')).toBeNull();
|
||||
expect(compiler.passObjectFromDumpFilename('example.dump')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('trimGccDumpHeaderFunctions', () => {
|
||||
const userBlock = [
|
||||
';; Function main (main, funcdef_no=1, decl_uid=1, cgraph_uid=1, symbol_order=1)',
|
||||
'[example.cpp:3:5] x = 1;',
|
||||
'',
|
||||
].join('\n');
|
||||
const headerBlock = [
|
||||
';; Function std::foo (_ZSt3foo, funcdef_no=2, decl_uid=2, cgraph_uid=2, symbol_order=2)',
|
||||
'[/usr/include/c++/13/foo.h:10:2] y = 2;',
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
// 4th arg = isRtlDump (RTL dumps are left untouched by the lineno strip).
|
||||
it('keeps source-defined functions and drops header-defined ones', () => {
|
||||
const trimmed = compiler.trimGccDumpHeaderFunctions(userBlock + headerBlock, 'example.cpp', true, false);
|
||||
expect(trimmed).toContain(';; Function main');
|
||||
expect(trimmed).not.toContain(';; Function std::foo');
|
||||
});
|
||||
|
||||
it('keeps a preamble before the first function', () => {
|
||||
const withPreamble = ';; preamble line\n' + headerBlock;
|
||||
const trimmed = compiler.trimGccDumpHeaderFunctions(withPreamble, 'example.cpp', true, false);
|
||||
expect(trimmed).toContain(';; preamble line');
|
||||
expect(trimmed).not.toContain(';; Function std::foo');
|
||||
});
|
||||
|
||||
it('returns dumps without function markers unchanged (e.g. IPA summaries)', () => {
|
||||
const cgraph = 'Callgraph:\nmain/1 (main)\n called by:\n';
|
||||
expect(compiler.trimGccDumpHeaderFunctions(cgraph, 'example.cpp', true, false)).toEqual(cgraph);
|
||||
});
|
||||
|
||||
it('keeps a function block whose origin cannot be determined (no header path)', () => {
|
||||
const noLoc = ';; Function mystery (mystery)\n some body\n';
|
||||
expect(compiler.trimGccDumpHeaderFunctions(noLoc, 'example.cpp', true, false)).toEqual(noLoc);
|
||||
});
|
||||
|
||||
it('keeps RTL blocks that reference the source via quotes/nested brackets (#regression)', () => {
|
||||
// RTL dumps don't use a clean leading [file:line]; they have "file":line and noise
|
||||
// like [orig:N] / [N [file:line]]. The block must still be kept because it references
|
||||
// the source file. A syntax-based "first location" match used to wrongly empty these.
|
||||
const rtlBlock = [
|
||||
';; Function main (main, funcdef_no=0)',
|
||||
'(note 1 0 3 [orig:117] NOTE_INSN_DELETED)',
|
||||
'(insn 16 14 17 (set (reg:SI r0) (mem:SI)) "example.cpp":5:100 [3 [example.cpp:5:100]])',
|
||||
'',
|
||||
].join('\n');
|
||||
const trimmed = compiler.trimGccDumpHeaderFunctions(rtlBlock, 'example.cpp', true, true);
|
||||
expect(trimmed).toContain(';; Function main');
|
||||
expect(trimmed).toContain('[orig:117]'); // RTL brackets left intact
|
||||
});
|
||||
|
||||
it('does not strip brackets from RTL dumps even when lineno is disabled', () => {
|
||||
const rtlBlock = ';; Function main (main)\n(note 1 0 3 [orig:117] "example.cpp":5)\n';
|
||||
const trimmed = compiler.trimGccDumpHeaderFunctions(rtlBlock, 'example.cpp', false, true);
|
||||
expect(trimmed).toContain('[orig:117]');
|
||||
});
|
||||
|
||||
it('strips location annotations from tree dumps when lineno is disabled', () => {
|
||||
const trimmed = compiler.trimGccDumpHeaderFunctions(userBlock, 'example.cpp', false, false);
|
||||
expect(trimmed).toContain('x = 1;');
|
||||
expect(trimmed).not.toContain('[example.cpp:3:5]');
|
||||
});
|
||||
|
||||
it('strips location annotations from IPA GIMPLE-body dumps (icf, inline, ...) when lineno is off', () => {
|
||||
// IPA passes that print GIMPLE bodies carry the same [file:line] prefixes as tree
|
||||
// dumps; they must be stripped too (isRtlDump=false), which the old tree-only gate missed.
|
||||
const ipaBody =
|
||||
';; Function main (main)\n[example.cpp:10:8] # DEBUG BEGIN_STMT\n[example.cpp:11:23] x = 1;\n';
|
||||
const trimmed = compiler.trimGccDumpHeaderFunctions(ipaBody, 'example.cpp', false, false);
|
||||
expect(trimmed).toContain('x = 1;');
|
||||
expect(trimmed).not.toContain('[example.cpp:10:8]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('processGccDumpOutput (enumeration path)', () => {
|
||||
const rootDir = '/tmp/ce-test';
|
||||
const inputFilename = path.join(rootDir, 'example.cpp');
|
||||
|
||||
const dumpFiles: Record<string, string> = {
|
||||
'example.cpp.005t.original': ';; Function main (main, funcdef_no=1)\n[example.cpp:1:1] original body\n',
|
||||
'example.cpp.081i.cp': ';; Function main (main, funcdef_no=1)\n[example.cpp:1:1] cp body\n',
|
||||
'example.cpp.099i.ipa-clones': '', // empty dump (pass ran but emitted nothing) -> excluded
|
||||
'example.cpp.255r.expand':
|
||||
';; Function main (main, funcdef_no=1)\n[example.cpp:1:1] expand body\n' +
|
||||
';; Function std::lib (_ZSt3lib)\n[/usr/include/lib.h:2:2] header body\n',
|
||||
};
|
||||
|
||||
function mockFs() {
|
||||
const names = [
|
||||
...Object.keys(dumpFiles),
|
||||
'example.cpp.s', // compiler output, must be excluded
|
||||
'example.cpp', // source, must be excluded
|
||||
'example.cpp.o',
|
||||
];
|
||||
vi.spyOn(fs, 'readdir').mockResolvedValue(names as unknown as any);
|
||||
vi.spyOn(utils, 'tryReadTextFile').mockImplementation(
|
||||
async (filename: string) => dumpFiles[path.basename(filename)],
|
||||
);
|
||||
}
|
||||
|
||||
const baseOpts = (): GccDumpOptions => ({
|
||||
opened: true,
|
||||
treeDump: true,
|
||||
rtlDump: true,
|
||||
ipaDump: true,
|
||||
dumpFlags: {
|
||||
gimpleFe: false,
|
||||
address: false,
|
||||
alias: false,
|
||||
slim: false,
|
||||
raw: false,
|
||||
details: false,
|
||||
stats: false,
|
||||
blocks: false,
|
||||
vops: false,
|
||||
lineno: true,
|
||||
uid: false,
|
||||
all: false,
|
||||
},
|
||||
});
|
||||
|
||||
it('enumerates produced dumps, orders by pass number, excludes non-dump files', async () => {
|
||||
mockFs();
|
||||
const result: any = {inputFilename, stderr: []};
|
||||
const output = await compiler.processGccDumpOutput(baseOpts(), result, true, 'example.s');
|
||||
|
||||
expect(output.all.map((p: any) => p.filename_suffix)).toEqual(['t.original', 'i.cp', 'r.expand']);
|
||||
expect(Object.keys(output.passDumps!).sort()).toEqual(['i.cp', 'r.expand', 't.original']);
|
||||
// No .s / source / .o leaked into the dropdown.
|
||||
expect(output.all.map((p: any) => p.name)).not.toContain('s (rtl)');
|
||||
// The empty ipa-clones dump is excluded from both the drop-down and the cache.
|
||||
expect(output.all.map((p: any) => p.filename_suffix)).not.toContain('i.ipa-clones');
|
||||
expect(output.passDumps).not.toHaveProperty('i.ipa-clones');
|
||||
});
|
||||
|
||||
it('trims header functions out of the cached dump contents', async () => {
|
||||
mockFs();
|
||||
const result: any = {inputFilename, stderr: []};
|
||||
const output = await compiler.processGccDumpOutput(baseOpts(), result, true, 'example.s');
|
||||
|
||||
expect(output.passDumps!['r.expand']).toContain(';; Function main');
|
||||
expect(output.passDumps!['r.expand']).not.toContain(';; Function std::lib');
|
||||
});
|
||||
|
||||
it('strips lineno annotations from tree dumps when Line Numbers is off', async () => {
|
||||
mockFs();
|
||||
const opts = baseOpts();
|
||||
opts.dumpFlags!.lineno = false; // user did NOT enable Line Numbers
|
||||
const result: any = {inputFilename, stderr: []};
|
||||
const output = await compiler.processGccDumpOutput(opts, result, true, 'example.s');
|
||||
|
||||
expect(output.passDumps!['t.original']).toContain('original body');
|
||||
expect(output.passDumps!['t.original']).not.toContain('[example.cpp:1:1]');
|
||||
});
|
||||
|
||||
it('keeps lineno annotations when Line Numbers is on', async () => {
|
||||
mockFs();
|
||||
const opts = baseOpts(); // baseOpts has lineno: true
|
||||
const result: any = {inputFilename, stderr: []};
|
||||
const output = await compiler.processGccDumpOutput(opts, result, true, 'example.s');
|
||||
|
||||
expect(output.passDumps!['t.original']).toContain('[example.cpp:1:1]');
|
||||
});
|
||||
|
||||
it('serves the selected pass content and enables syntax highlight', async () => {
|
||||
mockFs();
|
||||
const opts = baseOpts();
|
||||
opts.pass = {
|
||||
filename_suffix: 'r.expand',
|
||||
name: 'expand (rtl)',
|
||||
command_prefix: '-fdump-rtl-expand',
|
||||
selectedPass: null,
|
||||
};
|
||||
const result: any = {inputFilename, stderr: []};
|
||||
const output = await compiler.processGccDumpOutput(opts, result, true, 'example.s');
|
||||
|
||||
expect(output.selectedPass).toEqual(opts.pass);
|
||||
expect(output.currentPassOutput).toContain('expand body');
|
||||
expect(output.syntaxHighlight).toBe(true);
|
||||
});
|
||||
|
||||
it('clears the selection when the requested pass produced no dump', async () => {
|
||||
mockFs();
|
||||
const opts = baseOpts();
|
||||
opts.pass = {
|
||||
filename_suffix: 'r.doesnotexist',
|
||||
name: 'doesnotexist (rtl)',
|
||||
command_prefix: '-fdump-rtl-doesnotexist',
|
||||
selectedPass: null,
|
||||
};
|
||||
const result: any = {inputFilename, stderr: []};
|
||||
const output = await compiler.processGccDumpOutput(opts, result, true, 'example.s');
|
||||
|
||||
expect(output.selectedPass).toBeNull();
|
||||
});
|
||||
|
||||
it('respects the category filters (rtl only)', async () => {
|
||||
mockFs();
|
||||
const opts = baseOpts();
|
||||
opts.treeDump = false;
|
||||
opts.ipaDump = false;
|
||||
const result: any = {inputFilename, stderr: []};
|
||||
const output = await compiler.processGccDumpOutput(opts, result, true, 'example.s');
|
||||
|
||||
expect(output.all.map((p: any) => p.filename_suffix)).toEqual(['r.expand']);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -95,6 +95,17 @@ export type GccDumpOptions = {
|
||||
dumpFlags?: GccDumpFlags;
|
||||
};
|
||||
|
||||
export type GccDumpOutput = {
|
||||
all: GccDumpViewSelectedPass[];
|
||||
selectedPass: GccDumpViewSelectedPass | null;
|
||||
currentPassOutput: string;
|
||||
syntaxHighlight: boolean;
|
||||
// Maps a pass's filename_suffix to its (header-trimmed) dump content. Populated when all
|
||||
// passes are dumped and read at once (removeEmptyGccDump compilers). For libgccjit-based
|
||||
// compilers this will be an empty object (they still recompile per pass selection).
|
||||
passDumps: Record<string, string>;
|
||||
};
|
||||
|
||||
export type CompilationRequestOptions = {
|
||||
userArguments: string;
|
||||
compilerOptions: {
|
||||
@@ -186,7 +197,7 @@ export type CompilationResult = {
|
||||
dirPath?: string;
|
||||
compilationOptions?: string[];
|
||||
downloads?: BuildEnvDownloadInfo[];
|
||||
gccDumpOutput?;
|
||||
gccDumpOutput?: GccDumpOutput;
|
||||
languageId?: string;
|
||||
asmKeywordTypes?: string[];
|
||||
result?: CompilationResult; // cmake inner result
|
||||
|
||||
Reference in New Issue
Block a user