Repository: compiler-explorer/compiler-explorer
Pull request #6697 (2024-07-10 18:48:16)

Add EricWF's contracts implementation

AbrilRBS: Related infra PR: https://github.com/compiler-explorer/infra/pull/1363

Patch:
diff --git a/etc/config/c++.amazon.properties b/etc/config/c++.amazon.properties
index 4f21450c4..8362d88cb 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>
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,
+        };
     }
 }


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',
         );
     });