Repository: compiler-explorer/compiler-explorer
Pull request #6173 (2024-02-21 02:55:11)

Dynamic instruction set for documentation
Tries to determine the instruction set for documentation.
Not sure if I'm happy with it, but hey it's something.

Would fix https://github.com/compiler-explorer/infra/issues/1150
Would fix https://github.com/compiler-explorer/compiler-explorer/issues/3759

Does not work in situation if you enter the `--target` in a way that doesn't match the "possible override flags"


Patch:
diff --git a/lib/base-compiler.ts b/lib/base-compiler.ts
index 48db1f92f..1705b3c67 100644
--- a/lib/base-compiler.ts
+++ b/lib/base-compiler.ts
@@ -71,6 +71,7 @@ import {
     UnprocessedExecResult,
 } from '../types/execution/execution.interfaces.js';
 import type {CompilerOutputOptions, ParseFiltersAndOutputOptions} from '../types/features/filters.interfaces.js';
+import {InstructionSet} from '../types/instructionsets.js';
 import type {Language} from '../types/languages.interfaces.js';
 import type {Library, LibraryVersion, SelectedLibraryVersion} from '../types/libraries/libraries.interfaces.js';
 import type {ResultLine} from '../types/resultline/resultline.interfaces.js';
@@ -270,16 +271,16 @@ export class BaseCompiler implements ICompiler {
         if (!this.compiler.instructionSet) {
             const isets = new InstructionSets();
             if (this.buildenvsetup) {
-                isets
-                    .getCompilerInstructionSetHint(this.buildenvsetup.compilerArch, this.compiler.exe)
-                    .then(res => (this.compiler.instructionSet = res))
-                    .catch(() => {});
+                this.compiler.instructionSet = isets.getCompilerInstructionSetHint(
+                    this.buildenvsetup.compilerArch,
+                    this.compiler.exe,
+                );
             } else {
                 const temp = new BuildEnvSetupBase(this.compiler, this.env);
-                isets
-                    .getCompilerInstructionSetHint(temp.compilerArch, this.compiler.exe)
-                    .then(res => (this.compiler.instructionSet = res))
-                    .catch(() => {});
+                this.compiler.instructionSet = isets.getCompilerInstructionSetHint(
+                    temp.compilerArch,
+                    this.compiler.exe,
+                );
             }
         }
 
@@ -474,6 +475,63 @@ export class BaseCompiler implements ICompiler {
         return undefined;
     }
 
+    getTargetHintFromCompilerArgs(args: string[]): string | undefined {
+        const allFlags = this.getAllPossibleTargetFlags();
+
+        if (allFlags.length > 0) {
+            for (const possibleFlag of allFlags) {
+                // possible.flags contains either something like ['--target', '<value>'] or ['--target=<value>'], we want the flags without <value>
+                const filteredFlags: string[] = [];
+                let targetFlagOffset = -1;
+                for (const [i, flag] of possibleFlag.entries()) {
+                    if (flag.includes(c_value_placeholder)) {
+                        filteredFlags.push(flag.replace(c_value_placeholder, ''));
+                        targetFlagOffset = i;
+                    } else {
+                        filteredFlags.push(flag);
+                    }
+                }
+
+                if (targetFlagOffset === -1) continue;
+
+                // try to find matching flags in args
+                let foundFlag = -1;
+                for (const arg of args) {
+                    if (arg.startsWith(filteredFlags[foundFlag + 1])) {
+                        foundFlag = foundFlag + 1;
+                    }
+
+                    if (foundFlag === targetFlagOffset) {
+                        if (arg.length > filteredFlags[foundFlag].length) {
+                            return arg.substring(filteredFlags[foundFlag].length);
+                        }
+                        return arg;
+                    }
+                }
+            }
+        }
+
+        return undefined;
+    }
+
+    getInstructionSetFromCompilerArgs(args: string[]): InstructionSet {
+        try {
+            const archHint = this.getTargetHintFromCompilerArgs(args);
+            if (archHint) {
+                const isets = new InstructionSets();
+                return isets.getCompilerInstructionSetHint(archHint, this.compiler.exe);
+            }
+        } catch (e) {
+            logger.debug('Unexpected error in getInstructionSetFromCompilerArgs(): ', e);
+        }
+
+        if (this.compiler.instructionSet) {
+            return this.compiler.instructionSet;
+        } else {
+            return 'amd64';
+        }
+    }
+
     async runCompiler(
         compiler: string,
         options: string[],
@@ -493,6 +551,7 @@ export class BaseCompiler implements ICompiler {
         return {
             ...this.transformToCompilationResult(result, inputFilename),
             languageId: this.getCompilerResultLanguageId(filters),
+            instructionSet: this.getInstructionSetFromCompilerArgs(options),
         };
     }
 
@@ -3485,6 +3544,15 @@ but nothing was dumped. Possible causes are:
         return [];
     }
 
+    getAllPossibleTargetFlags(): string[][] {
+        const all: string[][] = [];
+        if (this.compiler.supportsMarch) all.push([`-march=${c_value_placeholder}`]);
+        if (this.compiler.supportsTargetIs) all.push([`--target=${c_value_placeholder}`]);
+        if (this.compiler.supportsTarget) all.push(['--target', c_value_placeholder]);
+
+        return all;
+    }
+
     async getPossibleToolchains(): Promise<CompilerOverrideOptions> {
         return this.env.getPossibleToolchains();
     }
diff --git a/lib/instructionsets.ts b/lib/instructionsets.ts
index bde67e2e2..c20006b14 100644
--- a/lib/instructionsets.ts
+++ b/lib/instructionsets.ts
@@ -80,15 +80,15 @@ export class InstructionSets {
                 path: ['/msp430-'],
             },
             powerpc: {
-                target: ['powerpc'],
+                target: ['powerpc', 'ppc64', 'ppc'],
                 path: ['/powerpc-', '/powerpc64-', '/powerpc64le-'],
             },
             riscv64: {
-                target: ['rv64'],
+                target: ['rv64', 'riscv64'],
                 path: ['/riscv64-'],
             },
             riscv32: {
-                target: ['rv32'],
+                target: ['rv32', 'riscv32'],
                 path: ['/riscv32-'],
             },
             sh: {
@@ -182,35 +182,31 @@ export class InstructionSets {
         };
     }
 
-    async getCompilerInstructionSetHint(compilerArch: string | boolean, exe: string): Promise<InstructionSet> {
-        return new Promise(resolve => {
-            if (compilerArch && typeof compilerArch === 'string') {
-                for (const [instructionSet, method] of Object.entries(this.supported) as [
-                    InstructionSet,
-                    InstructionSetMethod,
-                ][]) {
-                    for (const target of method.target) {
-                        if (compilerArch.includes(target)) {
-                            resolve(instructionSet);
-                            return;
-                        }
+    getCompilerInstructionSetHint(compilerArch: string | boolean, exe?: string): InstructionSet {
+        if (compilerArch && typeof compilerArch === 'string') {
+            for (const [instructionSet, method] of Object.entries(this.supported) as [
+                InstructionSet,
+                InstructionSetMethod,
+            ][]) {
+                for (const target of method.target) {
+                    if (compilerArch.includes(target)) {
+                        return instructionSet;
                     }
                 }
-            } else {
-                for (const [instructionSet, method] of Object.entries(this.supported) as [
-                    InstructionSet,
-                    InstructionSetMethod,
-                ][]) {
-                    for (const path of method.path) {
-                        if (exe.includes(path)) {
-                            resolve(instructionSet);
-                            return;
-                        }
+            }
+        } else {
+            for (const [instructionSet, method] of Object.entries(this.supported) as [
+                InstructionSet,
+                InstructionSetMethod,
+            ][]) {
+                for (const path of method.path) {
+                    if (exe?.includes(path)) {
+                        return instructionSet;
                     }
                 }
             }
+        }
 
-            resolve(this.defaultInstructionset);
-        });
+        return this.defaultInstructionset;
     }
 }
diff --git a/static/panes/compiler.ts b/static/panes/compiler.ts
index 1ca2fd655..c2aa56a33 100644
--- a/static/panes/compiler.ts
+++ b/static/panes/compiler.ts
@@ -176,6 +176,7 @@ export class Compiler extends MonacoPane<monaco.editor.IStandaloneCodeEditor, Co
     private compilerPickerElement: JQuery<HTMLElement>;
     private compilerPicker: CompilerPicker;
     private compiler: CompilerInfo | null;
+    private recentInstructionSet: InstructionSet | null;
     private currentLangId: string | null;
     private filters: Toggles;
     private optButton: JQuery<HTMLElementTagNameMap[keyof HTMLElementTagNameMap]>;
@@ -1469,6 +1470,8 @@ export class Compiler extends MonacoPane<monaco.editor.IStandaloneCodeEditor, Co
     }
 
     setAssembly(result: Partial<CompilationResult>, filteredCount = 0) {
+        this.recentInstructionSet = result.instructionSet || null;
+
         const asm = result.asm || this.fakeAsm('<No output>');
         this.assembly = asm;
         if (!this.editor.getModel()) return;
@@ -3615,7 +3618,10 @@ export class Compiler extends MonacoPane<monaco.editor.IStandaloneCodeEditor, Co
                 this.isWordAsmKeyword(e.target.position.lineNumber, currentWord)
             ) {
                 try {
-                    const response = await Compiler.getAsmInfo(currentWord.word, unwrap(this.compiler.instructionSet));
+                    const response = await Compiler.getAsmInfo(
+                        currentWord.word,
+                        unwrap(this.recentInstructionSet || this.compiler.instructionSet),
+                    );
                     if (!response) return;
                     this.decorations.asmToolTip = [
                         {
@@ -3691,7 +3697,10 @@ export class Compiler extends MonacoPane<monaco.editor.IStandaloneCodeEditor, Co
 
         try {
             if (this.compiler?.supportsAsmDocs) {
-                const asmHelp = await Compiler.getAsmInfo(word.word, unwrap(this.compiler.instructionSet));
+                const asmHelp = await Compiler.getAsmInfo(
+                    word.word,
+                    unwrap(this.recentInstructionSet || this.compiler.instructionSet),
+                );
                 if (asmHelp) {
                     this.alertSystem.alert(opcode + ' help', asmHelp.html + appendInfo(asmHelp.url), {
                         onClose: () => {
diff --git a/types/compilation/compilation.interfaces.ts b/types/compilation/compilation.interfaces.ts
index ec4f45530..87bf1a0ec 100644
--- a/types/compilation/compilation.interfaces.ts
+++ b/types/compilation/compilation.interfaces.ts
@@ -31,6 +31,7 @@ import {ParsedAsmResultLine} from '../asmresult/asmresult.interfaces.js';
 import {CompilerInfo} from '../compiler.interfaces.js';
 import {BasicExecutionResult, ConfiguredRuntimeTools} from '../execution/execution.interfaces.js';
 import {ParseFiltersAndOutputOptions} from '../features/filters.interfaces.js';
+import {InstructionSet} from '../instructionsets.js';
 import {ResultLine} from '../resultline/resultline.interfaces.js';
 import {Artifact, ToolResult} from '../tool.interfaces.js';
 
@@ -226,6 +227,8 @@ export type CompilationResult = {
     parsingTime?: number;
 
     source?: string; // todo: this is a crazy hack, we should get rid of it
+
+    instructionSet?: InstructionSet;
 };
 
 export type ExecutionOptions = {


Test patch:
diff --git a/test/base-compiler-tests.ts b/test/base-compiler-tests.ts
index 0ec6ad701..3150dacbe 100644
--- a/test/base-compiler-tests.ts
+++ b/test/base-compiler-tests.ts
@@ -27,7 +27,9 @@ import {beforeAll, describe, expect, it} from 'vitest';
 import {BaseCompiler} from '../lib/base-compiler.js';
 import {BuildEnvSetupBase} from '../lib/buildenvsetup/index.js';
 import {CompilationEnvironment} from '../lib/compilation-env.js';
+import {ClangCompiler} from '../lib/compilers/clang.js';
 import {Win32Compiler} from '../lib/compilers/win32.js';
+import {splitArguments} from '../lib/utils.js';
 import {CompilerOverrideType, ConfiguredOverrides} from '../types/compilation/compiler-overrides.interfaces.js';
 import {CompilerInfo} from '../types/compiler.interfaces.js';
 
@@ -714,3 +716,39 @@ describe('getDefaultExecOptions', () => {
         expect(paths).toEqual(['/usr/local/ninja', '/tmp/p1', '/tmp/p2']);
     });
 });
+
+describe('Target hints', () => {
+    let ce: CompilationEnvironment;
+
+    const noExecuteSupportCompilerInfo = makeFakeCompilerInfo({
+        exe: '/usr/bin/clang++',
+        lang: 'c++',
+        supportsTargetIs: true,
+        supportsTarget: true,
+        ldPath: [],
+        libPath: [],
+        extraPath: [],
+    });
+
+    beforeAll(() => {
+        ce = makeCompilationEnvironment({
+            languages,
+            props: {
+                environmentPassThrough: '',
+                ninjaPath: '/usr/local/ninja',
+            },
+        });
+    });
+
+    it('Should determine the target for Clang', async () => {
+        const compiler = new ClangCompiler(noExecuteSupportCompilerInfo, ce);
+
+        const args =
+            '-gdwarf-4 -g -o output.s -mllvm --x86-asm-syntax=intel -S --gcc-toolchain=/opt/compiler-explorer/gcc-13.2.0 -fcolor-diagnostics -fno-crash-diagnostics --target=riscv64 example.cpp -isystem/opt/compiler-explorer/libs/abseil';
+        const argArray = splitArguments(args);
+        const hint = compiler.getTargetHintFromCompilerArgs(argArray);
+        expect(hint).toBe('riscv64');
+        const iset = await compiler.getInstructionSetFromCompilerArgs(argArray);
+        expect(iset).toBe('riscv64');
+    });
+});
diff --git a/test/instructionsets-tests.ts b/test/instructionsets-tests.ts
index 544cd10d9..84f9a630e 100644
--- a/test/instructionsets-tests.ts
+++ b/test/instructionsets-tests.ts
@@ -26,31 +26,29 @@ import {describe, expect, it} from 'vitest';
 
 import {InstructionSets} from '../lib/instructionsets.js';
 
-describe('InstructionSets', async () => {
-    it('should recognize aarch64 for clang target', async () => {
+describe('InstructionSets', () => {
+    it('should recognize aarch64 for clang target', () => {
         const isets = new InstructionSets();
 
-        await expect(
+        expect(
             isets.getCompilerInstructionSetHint('aarch64-linux-gnu', '/opt/compiler-explorer/clang-11.0.1/bin/clang++'),
-        ).resolves.toEqual('aarch64');
+        ).toBe('aarch64');
     });
 
-    it('should recognize gcc aarch64 from filepath', async () => {
+    it('should recognize gcc aarch64 from filepath', () => {
         const isets = new InstructionSets();
 
-        await expect(
+        expect(
             isets.getCompilerInstructionSetHint(
                 false,
                 '/opt/compiler-explorer/arm64/gcc-12.1.0/aarch64-unknown-linux-gnu/bin/aarch64-unknown-linux-gnu-g++',
             ),
-        ).resolves.toEqual('aarch64');
+        ).toBe('aarch64');
     });
 
-    it('should default to amd64 when not apparent', async () => {
+    it('should default to amd64 when not apparent', () => {
         const isets = new InstructionSets();
 
-        await expect(
-            isets.getCompilerInstructionSetHint(false, '/opt/compiler-explorer/gcc-12.2.0/bin/g++'),
-        ).resolves.toEqual('amd64');
+        expect(isets.getCompilerInstructionSetHint(false, '/opt/compiler-explorer/gcc-12.2.0/bin/g++')).toBe('amd64');
     });
 });