From fa456a81f3f218b9cfdc4dfd249fd66cfda38b61 Mon Sep 17 00:00:00 2001 From: Ofek Date: Sat, 20 Jun 2026 16:06:25 +0300 Subject: [PATCH] Fix #7870: use GCC pass-dump files to enumerate passes (#8826) 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) --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- cypress/e2e/gccdump.cy.ts | 42 ++- lib/base-compiler.ts | 189 ++++++++++-- static/components.ts | 4 +- static/panes/gccdump-view.ts | 57 +++- test/gccdump-tests.ts | 310 ++++++++++++++++++++ types/compilation/compilation.interfaces.ts | 13 +- 6 files changed, 576 insertions(+), 39 deletions(-) create mode 100644 test/gccdump-tests.ts diff --git a/cypress/e2e/gccdump.cy.ts b/cypress/e2e/gccdump.cy.ts index e053d5149..3bab46301 100644 --- a/cypress/e2e/gccdump.cy.ts +++ b/cypress/e2e/gccdump.cy.ts @@ -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); + }); + }); + }); }); diff --git a/lib/base-compiler.ts b/lib/base-compiler.ts index 9b468db8a..a5874378f 100644 --- a/lib/base-compiler.ts +++ b/lib/base-compiler.ts @@ -741,7 +741,7 @@ export class BaseCompiler { } getGccDumpOptions(gccDumpOptions: Record, 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 (`..`, 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 (...)` 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, }; } @@ -3600,7 +3680,79 @@ export class BaseCompiler { selectedPass: opts.pass ?? null, currentPassOutput: '', syntaxHighlight: false, + passDumps: {} as Record, }; + + 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(); + if (opts.treeDump) selectedCategories.add('tree'); + if (opts.ipaDump) selectedCategories.add('ipa'); + if (opts.rtlDump) selectedCategories.add('rtl'); + const categoryByLetter: Record = {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--=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; } diff --git a/static/components.ts b/static/components.ts index 931775c68..068079075 100644 --- a/static/components.ts +++ b/static/components.ts @@ -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 { return { type: 'component', diff --git a/static/panes/gccdump-view.ts b/static/panes/gccdump-view.ts index 23c536464..837c1a8cd 100644 --- a/static/panes/gccdump-view.ts +++ b/static/panes/gccdump-view.ts @@ -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 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 = {}; constructor(hub: Hub, container: Container, state: GccDumpViewState & MonacoPaneState) { super(hub, container, state); @@ -171,7 +175,7 @@ export class GccDump extends MonacoPane); @@ -313,7 +317,16 @@ export class GccDump extends MonacoPane 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 { + 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 = { + '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']); + }); + }); +}); diff --git a/types/compilation/compilation.interfaces.ts b/types/compilation/compilation.interfaces.ts index b11dafb53..2735e7edc 100644 --- a/types/compilation/compilation.interfaces.ts +++ b/types/compilation/compilation.interfaces.ts @@ -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; +}; + 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