Repository: compiler-explorer/compiler-explorer
Pull request #6698 (2024-07-11 19:54:29)

 Set default min API to 27 for R8/D8
The minimum API is currently not set, meaning that some bytecodes are
mising (invoke-polymorphic from API level 26; const-method-handle and
const-method-type from API level 27). This change sets a default minimum
API level of 27, while allowing for the option to still be overridden.


Patch:
diff --git a/etc/config/c++.amazon.properties b/etc/config/c++.amazon.properties
index 4f21450c4..c0974f18a 100644
--- a/etc/config/c++.amazon.properties
+++ b/etc/config/c++.amazon.properties
@@ -415,7 +415,7 @@ compiler.clang1810.options=--gcc-toolchain=/opt/compiler-explorer/gcc-13.2.0
 compiler.clang1810.ldPath=${exePath}/../lib|${exePath}/../lib/x86_64-unknown-linux-gnu
 compiler.clang1810.debugPatched=true
 
-group.clangx86trunk.compilers=clang_trunk:clang_assertions_trunk:clang_concepts:clang_p1144:clang_autonsdmi:clang_bb_p2996:clang_p3068:clang_p3309:clang_lifetime:clang_p1061:clang_parmexpr:clang_patmat:clang_embed:clang_dang:clang_reflection:clang_variadic_friends:clang_widberg:clang_resugar:clang_clangir:clang_dascandy_contracts:clang_p1974
+group.clangx86trunk.compilers=clang_trunk:clang_assertions_trunk:clang_concepts:clang_p1144:clang_autonsdmi:clang_bb_p2996:clang_p3068:clang_p3309:clang_lifetime:clang_p1061:clang_parmexpr:clang_patmat:clang_embed:clang_dang:clang_reflection:clang_variadic_friends:clang_widberg:clang_resugar:clang_clangir:clang_dascandy_contracts:clang_ericwf_contracts:clang_p1974
 group.clangx86trunk.intelAsm=-mllvm --x86-asm-syntax=intel
 group.clangx86trunk.options=--gcc-toolchain=/opt/compiler-explorer/gcc-snapshot
 group.clangx86trunk.objdumper=/opt/compiler-explorer/gcc-snapshot/bin/objdump
@@ -507,6 +507,9 @@ compiler.clang_clangir.notification=ClangIR enabled clang compiler; see <a href=
 compiler.clang_dascandy_contracts.exe=/opt/compiler-explorer/clang-dascandy-contracts-trunk/bin/clang++
 compiler.clang_dascandy_contracts.semver=(dascandy contracts)
 compiler.clang_dascandy_contracts.notification=Dascandy's contracts compiler; see <a href="https://github.com/dascandy/llvm-project/tree/add-contracts" target="_blank" rel="noopener noreferrer">dascandy/llvm-project<sup><small class="fas fa-external-link-alt opens-new-window" title="Opens in a new window"></small></sup></a>
+compiler.clang_ericwf_contracts.exe=/opt/compiler-explorer/clang-ericwf-contracts-trunk/bin/clang++
+compiler.clang_ericwf_contracts.semver=(EricWF contracts)
+compiler.clang_ericwf_contracts.notification=EricWF's contracts compiler; see <a href="https://github.com/efcs/llvm-project/tree/contracts-nightly" target="_blank" rel="noopener noreferrer">efcs/llvm-project<sup><small class="fas fa-external-link-alt opens-new-window" title="Opens in a new window"></small></sup></a>
 compiler.clang_p1974.exe=/opt/compiler-explorer/clang-p1974-trunk/bin/clang++
 compiler.clang_p1974.semver=(p1974)
 compiler.clang_p1974.notification=p1974; see <a href="https://github.com/je4d/llvm-project/tree/godbolt/propconst" target="_blank" rel="noopener noreferrer">je4d/llvm-project<sup><small class="fas fa-external-link-alt opens-new-window" title="Opens in a new window"></small></sup></a>
@@ -4578,11 +4581,12 @@ libs.magic_enum.versions.073.version=0.7.3
 libs.magic_enum.versions.073.path=/opt/compiler-explorer/libs/magic_enum/v0.7.3/include
 
 libs.mfem.name=mfem
-libs.mfem.versions=trunk
+libs.mfem.versions=47
 libs.mfem.url=https://mfem.org
 libs.mfem.staticliblink=mfem
-libs.mfem.versions.trunk.version=trunk
-libs.mfem.versions.trunk.path=/opt/compiler-explorer/libs/mfem/trunk
+libs.mfem.description=MFEM is a free, lightweight, scalable C++ library for finite element methods.<br /><br />Note that the trunk version is no longer available, use v4.7 instead.
+libs.mfem.versions.47.version=4.7
+libs.mfem.versions.47.packagedheaders=true
 
 libs.mlir.name=MLIR
 libs.mlir.description=MLIR
@@ -4935,8 +4939,10 @@ libs.stdexec.versions.trunk.path=/opt/compiler-explorer/libs/stdexec/trunk/inclu
 
 libs.strong_type.name=strong_type
 libs.strong_type.description=library for creating strong types in C++
-libs.strong_type.versions=14:13:12:11:10:9:8:7:6:5:4:3:2:1
+libs.strong_type.versions=15:14:13:12:11:10:9:8:7:6:5:4:3:2:1
 libs.strong_type.url=https://github.com/rollbear/strong_type
+libs.strong_type.versions.15.path=/opt/compiler-explorer/libs/strong_type/v15/include
+libs.strong_type.versions.15.version=v15
 libs.strong_type.versions.14.path=/opt/compiler-explorer/libs/strong_type/v14/include
 libs.strong_type.versions.14.version=v14
 libs.strong_type.versions.13.path=/opt/compiler-explorer/libs/strong_type/v13/include
@@ -5030,8 +5036,10 @@ libs.tomlplusplus.versions.124.version=1.2.4
 libs.tomlplusplus.versions.124.path=/opt/compiler-explorer/libs/tomlplusplus/v1.2.4/include
 
 libs.trompeloeil.name=trompeloeil
-libs.trompeloeil.versions=47:46:45:44:43:42:41:40:39:38:37:36:35:34:33:32:31:30:29:28
+libs.trompeloeil.versions=48:47:46:45:44:43:42:41:40:39:38:37:36:35:34:33:32:31:30:29:28
 libs.trompeloeil.url=https://github.com/rollbear/trompeloeil
+libs.trompeloeil.versions.48.path=/opt/compiler-explorer/libs/trompeloeil/v48/include
+libs.trompeloeil.versions.48.version=v48
 libs.trompeloeil.versions.47.path=/opt/compiler-explorer/libs/trompeloeil/v47/include
 libs.trompeloeil.versions.47.version=v47
 libs.trompeloeil.versions.46.path=/opt/compiler-explorer/libs/trompeloeil/v46/include
diff --git a/etc/config/circle.amazon.properties b/etc/config/circle.amazon.properties
index 788de9b57..98efbaec6 100644
--- a/etc/config/circle.amazon.properties
+++ b/etc/config/circle.amazon.properties
@@ -1018,8 +1018,10 @@ libs.tomlplusplus.versions.124.version=1.2.4
 libs.tomlplusplus.versions.124.path=/opt/compiler-explorer/libs/tomlplusplus/v1.2.4/include
 
 libs.trompeloeil.name=trompeloeil
-libs.trompeloeil.versions=47:46:45:44:43:42:41:40:39:38:37:36:35:34:33:32:31:30:29:28
+libs.trompeloeil.versions=48:47:46:45:44:43:42:41:40:39:38:37:36:35:34:33:32:31:30:29:28
 libs.trompeloeil.url=https://github.com/rollbear/trompeloeil
+libs.trompeloeil.versions.48.path=/opt/compiler-explorer/libs/trompeloeil/v48/include
+libs.trompeloeil.versions.48.version=v48
 libs.trompeloeil.versions.47.path=/opt/compiler-explorer/libs/trompeloeil/v47/include
 libs.trompeloeil.versions.47.version=v47
 libs.trompeloeil.versions.46.path=/opt/compiler-explorer/libs/trompeloeil/v46/include
diff --git a/etc/config/objc++.amazon.properties b/etc/config/objc++.amazon.properties
index 303a8befd..f1173c03a 100644
--- a/etc/config/objc++.amazon.properties
+++ b/etc/config/objc++.amazon.properties
@@ -1741,8 +1741,10 @@ libs.stdexec.versions.trunk.path=/opt/compiler-explorer/libs/stdexec/trunk/inclu
 
 libs.strong_type.name=strong_type
 libs.strong_type.description=library for creating strong types in C++
-libs.strong_type.versions=14:13:12:11:10:9:8:7:6:5:4:3:2:1
+libs.strong_type.versions=15:14:13:12:11:10:9:8:7:6:5:4:3:2:1
 libs.strong_type.url=https://github.com/rollbear/strong_type
+libs.strong_type.versions.15.path=/opt/compiler-explorer/libs/strong_type/v15/include
+libs.strong_type.versions.15.version=v15
 libs.strong_type.versions.14.path=/opt/compiler-explorer/libs/strong_type/v14/include
 libs.strong_type.versions.14.version=v14
 libs.strong_type.versions.13.path=/opt/compiler-explorer/libs/strong_type/v13/include
@@ -1822,8 +1824,10 @@ libs.tomlplusplus.versions.124.version=1.2.4
 libs.tomlplusplus.versions.124.path=/opt/compiler-explorer/libs/tomlplusplus/v1.2.4/include
 
 libs.trompeloeil.name=trompeloeil
-libs.trompeloeil.versions=47:46:45:44:43:42:41:40:39:38:37:36:35:34:33:32:31:30:29:28
+libs.trompeloeil.versions=48:47:46:45:44:43:42:41:40:39:38:37:36:35:34:33:32:31:30:29:28
 libs.trompeloeil.url=https://github.com/rollbear/trompeloeil
+libs.trompeloeil.versions.48.path=/opt/compiler-explorer/libs/trompeloeil/v48/include
+libs.trompeloeil.versions.48.version=v48
 libs.trompeloeil.versions.47.path=/opt/compiler-explorer/libs/trompeloeil/v47/include
 libs.trompeloeil.versions.47.version=v47
 libs.trompeloeil.versions.46.path=/opt/compiler-explorer/libs/trompeloeil/v46/include
diff --git a/lib/compilers/d8.ts b/lib/compilers/d8.ts
index a5532da1a..df1a6e782 100644
--- a/lib/compilers/d8.ts
+++ b/lib/compilers/d8.ts
@@ -47,6 +47,8 @@ export class D8Compiler extends BaseCompiler implements SimpleOutputFilenameComp
     lineNumberRegex: RegExp;
     methodEndRegex: RegExp;
 
+    minApiArgRegex: RegExp;
+
     javaId: string;
     kotlinId: string;
 
@@ -58,6 +60,8 @@ export class D8Compiler extends BaseCompiler implements SimpleOutputFilenameComp
         this.lineNumberRegex = /^\s+\.line\s+(\d+).*$/;
         this.methodEndRegex = /^\s*\.end\smethod.*$/;
 
+        this.minApiArgRegex = /^--min-api$/;
+
         this.javaId = this.compilerProps<string>(`group.${this.compiler.group}.javaId`);
         this.kotlinId = this.compilerProps<string>(`group.${this.compiler.group}.kotlinId`);
 
@@ -142,10 +146,18 @@ export class D8Compiler extends BaseCompiler implements SimpleOutputFilenameComp
             execOptions.customCwd = path.dirname(inputFilename);
         }
 
+        let useDefaultMinApi = true;
+
         // The items in 'options' before the source file are user inputs.
         const sourceFileOptionIndex = options.findIndex(option => {
             return option.endsWith('.java') || option.endsWith('.kt');
         });
+        const userOptions = options.slice(0, sourceFileOptionIndex);
+        for (const option of userOptions) {
+            if (this.minApiArgRegex.test(option)) {
+                useDefaultMinApi = false;
+            }
+        }
 
         const files = await fs.readdir(preliminaryCompilePath, {encoding: 'utf8', recursive: true});
         const classFiles = files.filter(f => f.endsWith('.class'));
@@ -153,7 +165,8 @@ export class D8Compiler extends BaseCompiler implements SimpleOutputFilenameComp
             '-cp',
             this.compiler.exe, // R8 jar.
             'com.android.tools.r8.D8', // Main class name for the D8 compiler.
-            ...options.slice(0, sourceFileOptionIndex),
+            ...userOptions,
+            ...this.getMinApiArgument(useDefaultMinApi),
             ...classFiles,
         ];
         const result = await this.exec(javaCompiler.javaRuntime, d8Options, execOptions);
@@ -236,6 +249,10 @@ export class D8Compiler extends BaseCompiler implements SimpleOutputFilenameComp
         return libString ? ['-cp', libString] : [''];
     }
 
+    getMinApiArgument(useDefaultMinApi: boolean): string[] {
+        return useDefaultMinApi ? ['--min-api', '27'] : [''];
+    }
+
     override getIncludeArguments(libraries: SelectedLibraryVersion[], dirPath: string): string[] {
         this.libPaths = libraries.flatMap(selectedLib => {
             const foundVersion = this.findLibVersion(selectedLib);
diff --git a/lib/compilers/r8.ts b/lib/compilers/r8.ts
index 9ca32585a..511f75028 100644
--- a/lib/compilers/r8.ts
+++ b/lib/compilers/r8.ts
@@ -124,10 +124,18 @@ export class R8Compiler extends D8Compiler implements SimpleOutputFilenameCompil
             execOptions.customCwd = path.dirname(inputFilename);
         }
 
+        let useDefaultMinApi = true;
+
         // The items in 'options' before the source file are user inputs.
         const sourceFileOptionIndex = options.findIndex(option => {
             return option.endsWith('.java') || option.endsWith('.kt');
         });
+        const userOptions = options.slice(0, sourceFileOptionIndex);
+        for (const option of userOptions) {
+            if (this.minApiArgRegex.test(option)) {
+                useDefaultMinApi = false;
+            }
+        }
 
         const files = await fs.readdir(preliminaryCompilePath);
         const classFiles = files.filter(f => f.endsWith('.class'));
@@ -137,7 +145,8 @@ export class R8Compiler extends D8Compiler implements SimpleOutputFilenameCompil
             this.compiler.exe, // R8 jar.
             'com.android.tools.r8.R8',
             ...this.getR8LibArguments(),
-            ...options.slice(0, sourceFileOptionIndex),
+            ...userOptions,
+            ...this.getMinApiArgument(useDefaultMinApi),
             ...classFiles,
         ];
         const result = await this.exec(javaCompiler.javaRuntime, r8Options, execOptions);
diff --git a/lib/demangler/base.ts b/lib/demangler/base.ts
index b0d8b3435..3fee52651 100644
--- a/lib/demangler/base.ts
+++ b/lib/demangler/base.ts
@@ -84,26 +84,6 @@ export class BaseDemangler extends AsmRegex {
         return !!this.demanglerExe;
     }
 
-    // Iterates over the labels, demangle the label names and updates the start and
-    // end position of the label.
-    protected demangleLabels(labels, tree: PrefixTree) {
-        if (!Array.isArray(labels) || labels.length === 0) return;
-
-        for (const [index, label] of labels.entries()) {
-            const value = label.name;
-            const newValue = tree.findExact(value);
-            if (newValue) {
-                label.name = newValue;
-                label.range.endCol = label.range.startCol + newValue.length;
-
-                // Update the startCol value for each further labels.
-                for (let j = index + 1; j < labels.length; j++) {
-                    labels[j].range.startCol += newValue.length - value.length;
-                }
-            }
-        }
-    }
-
     protected demangleLabelDefinitions(labelDefinitions, translations: [string, string][]) {
         if (!labelDefinitions) return;
 
@@ -196,10 +176,18 @@ export class BaseDemangler extends AsmRegex {
         ].filter(elem => elem[0] !== elem[1]);
         if (translations.length > 0) {
             const tree = new PrefixTree(translations);
-
             for (const asm of this.result.asm) {
-                asm.text = tree.replaceAll(asm.text);
-                this.demangleLabels(asm.labels, tree);
+                const {newText, mapRanges, mapNames} = tree.replaceAll(asm.text);
+                asm.text = newText;
+                // We try to represent in labels.ranges the exact modifications done by replaceAll,
+                // but replaceAll sometimes modifies substrings not recognized as labels :(
+                if (asm.labels) {
+                    for (const label of asm.labels) {
+                        if (mapRanges[label.range.startCol])
+                            label.range = mapRanges[label.range.startCol][label.range.endCol] || label.range;
+                        label.name = mapNames[label.name] || label.name;
+                    }
+                }
             }
 
             this.demangleLabelDefinitions(this.result.labelDefinitions, translations);
diff --git a/lib/demangler/llvm.ts b/lib/demangler/llvm.ts
index c5f7915b3..d55446184 100644
--- a/lib/demangler/llvm.ts
+++ b/lib/demangler/llvm.ts
@@ -81,12 +81,12 @@ export class LLVMIRDemangler extends BaseDemangler {
         if (translations.length > 0) {
             const tree = new PrefixTree(translations);
             for (const [functionName, passes] of Object.entries(passOutput)) {
-                const demangledFunctionName = tree.replaceAll(functionName);
+                const demangledFunctionName = tree.replaceAll(functionName).newText;
                 for (const pass of passes) {
-                    pass.name = tree.replaceAll(pass.name); // needed at least for full module mode
+                    pass.name = tree.replaceAll(pass.name).newText; // needed at least for full module mode
                     for (const dump of [pass.before, pass.after]) {
                         for (const line of dump) {
-                            line.text = tree.replaceAll(line.text);
+                            line.text = tree.replaceAll(line.text).newText;
                         }
                     }
                 }
diff --git a/lib/demangler/prefix-tree.ts b/lib/demangler/prefix-tree.ts
index 6e1ad28d7..9cd414cd4 100644
--- a/lib/demangler/prefix-tree.ts
+++ b/lib/demangler/prefix-tree.ts
@@ -37,6 +37,17 @@ import {assert} from '../assert.js';
 
 type Node = Node[] & {result?: string};
 
+type charRange = {
+    startCol: number;
+    endCol: number;
+};
+
+export type replaceAction = {
+    newText: string;
+    mapRanges: Record<number, Record<number, charRange>>;
+    mapNames: Record<string, string>;
+};
+
 export class PrefixTree {
     root: Node = [];
 
@@ -86,25 +97,47 @@ export class PrefixTree {
     }
 
     // Replace all matches (longest match first) in a line.
-    replaceAll(line: string) {
-        let result = '';
-        let index = 0;
+    replaceAll(line: string): replaceAction {
+        let newText = '';
+        let idxInOld = 0;
+        let idxInNew = 0;
+        const mapRanges = {};
+        const mapNames = {};
         // Loop over each possible replacement point in the line.
         // Use a binary search to find the replacements (allowing a prefix match). If we couldn't find a match, skip
         // on, else use the replacement, and skip by that amount.
-        while (index < line.length) {
-            const lineBit = line.substring(index);
+        while (idxInOld < line.length) {
+            const lineBit = line.substring(idxInOld);
             const [oldValue, newValue] = this.findLongestMatch(lineBit);
             if (oldValue) {
                 // We found a replacement.
-                result += newValue;
-                index += oldValue.length;
+                newText += newValue;
+                mapNames[oldValue] = newValue;
+
+                idxInOld += oldValue.length;
+                idxInNew += newValue.length;
+
+                // The annoying +1 ultimately comes from monaco.Position being 1-based.
+                const oldStart = idxInOld - oldValue.length + 1;
+                const oldEnd = idxInOld + 1;
+                const newStart = idxInNew - newValue.length + 1;
+                const newEnd = idxInNew + 1;
+
+                // JS/TS don't allow using an object such as {oldStart, oldEnd} as a key in a dictionary/Map,
+                // we use a nested dictionary instead.
+                if (!mapRanges[oldStart]) mapRanges[oldStart] = {};
+                mapRanges[oldStart][oldEnd] = {startCol: newStart, endCol: newEnd};
             } else {
                 // No match; output the unmatched character, and keep looking.
-                result += line[index];
-                index++;
+                newText += line[idxInOld];
+                idxInOld++;
+                idxInNew++;
             }
         }
-        return result;
+        return {
+            newText: newText,
+            mapRanges: mapRanges,
+            mapNames: mapNames,
+        };
     }
 }
diff --git a/static/settings.ts b/static/settings.ts
index 57b57aa84..6520a5136 100644
--- a/static/settings.ts
+++ b/static/settings.ts
@@ -212,7 +212,7 @@ export class Settings {
     // The delay between a change and re-compilation is a substantial factor in site traffic load.
     // We want to be able to control it centrally - but only for users that didn't explicitly set it.
     // This is the place.
-    private defaultDelayAfterChange = 1500;
+    private defaultDelayAfterChange = 2000;
 
     constructor(
         hub: Hub,
diff --git a/static/widgets/fontscale.ts b/static/widgets/fontscale.ts
index e9b2ebabc..6a972e1d8 100644
--- a/static/widgets/fontscale.ts
+++ b/static/widgets/fontscale.ts
@@ -61,9 +61,9 @@ function makeFontSizeDropdown(elem: JQuery, obj: FontScale, buttonDropdown: JQue
     buttonDropdown.on('wheel', (e: any) => {
         e.preventDefault();
         let selectedId = elem.find('.active').index();
-        if (e.originalEvent.deltaY >= 0 && selectedId < elem.children().length - 1) {
+        if (e.originalEvent.deltaY < 0 && selectedId < elem.children().length - 1) {
             selectedId++;
-        } else if (e.originalEvent.deltaY < 0 && selectedId > 0) {
+        } else if (e.originalEvent.deltaY >= 0 && selectedId > 0) {
             selectedId--;
         }
         elem.children().eq(selectedId).trigger('click');


Test patch:
diff --git a/test/demangler-tests.ts b/test/demangler-tests.ts
index 530e71242..25ed40b6c 100644
--- a/test/demangler-tests.ts
+++ b/test/demangler-tests.ts
@@ -354,27 +354,27 @@ describe('Demangler prefix tree', () => {
     replacements.add('aa', 'long_a');
     replacements.add('aa_shouldnotmatch', 'ERROR');
     it('should replace a short match', () => {
-        expect(replacements.replaceAll('a')).toEqual('short_a');
+        expect(replacements.replaceAll('a').newText).toEqual('short_a');
     });
     it('should replace using the longest match', () => {
-        expect(replacements.replaceAll('aa')).toEqual('long_a');
+        expect(replacements.replaceAll('aa').newText).toEqual('long_a');
     });
     it('should replace using both', () => {
-        expect(replacements.replaceAll('aaa')).toEqual('long_ashort_a');
+        expect(replacements.replaceAll('aaa').newText).toEqual('long_ashort_a');
     });
     it('should replace using both', () => {
-        expect(replacements.replaceAll('a aa a aa')).toEqual('short_a long_a short_a long_a');
+        expect(replacements.replaceAll('a aa a aa').newText).toEqual('short_a long_a short_a long_a');
     });
     it('should work with empty replacements', () => {
-        expect(new PrefixTree([]).replaceAll('Testing 123')).toEqual('Testing 123');
+        expect(new PrefixTree([]).replaceAll('Testing 123').newText).toEqual('Testing 123');
     });
     it('should leave unmatching text alone', () => {
-        expect(replacements.replaceAll('Some text with none of the first letter of the ordered letter list')).toEqual(
-            'Some text with none of the first letter of the ordered letter list',
-        );
+        expect(
+            replacements.replaceAll('Some text with none of the first letter of the ordered letter list').newText,
+        ).toEqual('Some text with none of the first letter of the ordered letter list');
     });
     it('should handle a mixture', () => {
-        expect(replacements.replaceAll('Everyone loves an aardvark')).toEqual(
+        expect(replacements.replaceAll('Everyone loves an aardvark').newText).toEqual(
             'Everyone loves short_an long_ardvshort_ark',
         );
     });