Repository: compiler-explorer/compiler-explorer (ts)

Issue:
[BUG]: Incorrect control flow graph for Xtensa LX7 ISA
### Describe the bug

The control flow graph is incorrect for a trivial C function targeting the Xtensa LX7 ISA. A single node can contain more than one conditional jump. E.g.

<img width="388" alt="Screenshot 2024-09-23 at 12 35 10" src="https://github.com/user-attachments/assets/aac780fe-1a41-4b32-a84c-9b8bf3f74eec">

A node with an unconditional jump has more than one outgoing edge.

<img width="414" alt="Screenshot 2024-09-23 at 12 39 39" src="https://github.com/user-attachments/assets/4fbc6e35-6104-4e75-9784-680e8355836b">

It appears the ISA isn't fully covered by the CFG generator and is unaware of some jump instructions, resulting in incorrect CFG nodes and edges.

### Steps to reproduce

https://godbolt.org/z/Yd1bncTn3

See nodes for labels `.L19` (multiple jump instructions in a single node), `.L28` (unconditional jump with 2 outgoing edges), `.L11` (unconditional jump with 2 outgoing edges)

### Expected behavior

A CFG node should only have one (un-)conditional jump instructions in it. A CFG node with an unconditional jump instruction should not have more than one outgoing edge.

### Reproduction link

https://godbolt.org/z/Yd1bncTn3

### Screenshots

See description.

### Operating System

_No response_

### Browser version

_No response_

Pull request description:
Fix #6886: Add cfg support for Xtensa ISA
<img width="1708" height="928" alt="image" src="https://github.com/user-attachments/assets/c3ce2b13-bce5-4acc-aa80-659f7c78ccd0" />

Source taken from the link in #6886

Patch:
diff --git a/lib/cfg/instruction-sets/_all.ts b/lib/cfg/instruction-sets/_all.ts
index e9f3cc6e0..6caad4cb3 100644
--- a/lib/cfg/instruction-sets/_all.ts
+++ b/lib/cfg/instruction-sets/_all.ts
@@ -24,5 +24,6 @@
 
 export {ArmInstructionSetInfo} from './arm.js';
 export {BaseInstructionSetInfo} from './base.js';
+export {XtensaInstructionSetInfo} from './xtensa.js';
 export {LlvmIrInstructionSetInfo} from './llvm-ir.js';
 export {PythonInstructionSetInfo} from './python.js';
diff --git a/lib/cfg/instruction-sets/xtensa.ts b/lib/cfg/instruction-sets/xtensa.ts
new file mode 100644
index 000000000..0a6efce61
--- /dev/null
+++ b/lib/cfg/instruction-sets/xtensa.ts
@@ -0,0 +1,52 @@
+// Copyright (c) 2025, 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 {InstructionSet} from '../../../types/instructionsets.js';
+import {BaseInstructionSetInfo, InstructionType} from './base.js';
+
+export class XtensaInstructionSetInfo extends BaseInstructionSetInfo {
+    static override get key(): InstructionSet {
+        return 'xtensa';
+    }
+
+    // Full list:  BALL, BNALL, BANY, BNONE, BBC, BBCI, BBS, BBSI, BEQ, BEQI, BEQZ, BNE, BNEI, BNEZ
+    // BGE, BGEI, BGEU, BGEUI, BGEZ, BLT, BLTI, BLTU, BLTUI, BLTZ
+    static conditionalJumps = /^\s*b/;
+
+    // whitespaces followed by 'j' or 'jx'
+    static unconditionalJumps = /^\s*jx?/;
+    static returnInstruction = /^\s*ret/;
+
+    override isJmpInstruction(instruction: string) {
+        const instype = this.getInstructionType(instruction);
+        return instype === InstructionType.jmp || instype === InstructionType.conditionalJmpInst;
+    }
+
+    override getInstructionType(instruction: string) {
+        if (XtensaInstructionSetInfo.conditionalJumps.test(instruction)) return InstructionType.conditionalJmpInst;
+        if (XtensaInstructionSetInfo.unconditionalJumps.test(instruction)) return InstructionType.jmp;
+        if (XtensaInstructionSetInfo.returnInstruction.test(instruction)) return InstructionType.retInst;
+        return InstructionType.notRetInst;
+    }
+}


Test patch:
diff --git a/test/cfg-cases/cfg-xtensa.loop.json b/test/cfg-cases/cfg-xtensa.loop.json
new file mode 100644
index 000000000..eed500f5a
--- /dev/null
+++ b/test/cfg-cases/cfg-xtensa.loop.json
@@ -0,0 +1,177 @@
+{
+    "asm": [
+        {
+            "text": "square(int, int*, int&):",
+            "source": null,
+            "labels": []
+        },
+        {
+            "text": "        entry   sp, 32",
+            "source": {
+                "file": null,
+                "line": 2,
+                "column": 37
+            },
+            "labels": []
+        },
+        {
+            "text": "        blti    a2, 1, .L1",
+            "source": {
+                "file": null,
+                "line": 3,
+                "column": 20
+            },
+            "labels": [
+                {
+                    "name": ".L1",
+                    "range": {
+                        "startCol": 24,
+                        "endCol": 27
+                    }
+                }
+            ]
+        },
+        {
+            "text": "        slli    a8, a2, 2",
+            "source": {
+                "file": null,
+                "line": 3,
+                "column": 20
+            },
+            "labels": []
+        },
+        {
+            "text": "        addi    a8, a8, -4",
+            "source": {
+                "file": null,
+                "line": 3,
+                "column": 20
+            },
+            "labels": []
+        },
+        {
+            "text": "        srli    a8, a8, 2",
+            "source": {
+                "file": null,
+                "line": 3,
+                "column": 20
+            },
+            "labels": []
+        },
+        {
+            "text": "        addi.n  a8, a8, 1",
+            "source": {
+                "file": null,
+                "line": 3,
+                "column": 20
+            },
+            "labels": []
+        },
+        {
+            "text": "        loop    a8, .L3_LEND",
+            "source": {
+                "file": null,
+                "line": 3,
+                "column": 20
+            },
+            "labels": []
+        },
+        {
+            "text": "        l32i.n  a2, a3, 0",
+            "source": {
+                "file": null,
+                "line": 4,
+                "column": 14
+            },
+            "labels": []
+        },
+        {
+            "text": "        l32i.n  a9, a4, 0",
+            "source": {
+                "file": null,
+                "line": 4,
+                "column": 14
+            },
+            "labels": []
+        },
+        {
+            "text": "        add.n   a2, a2, a9",
+            "source": {
+                "file": null,
+                "line": 4,
+                "column": 14
+            },
+            "labels": []
+        },
+        {
+            "text": "        s32i.n  a2, a3, 0",
+            "source": {
+                "file": null,
+                "line": 4,
+                "column": 14
+            },
+            "labels": []
+        },
+        {
+            "text": "        addi.n  a3, a3, 4",
+            "source": {
+                "file": null,
+                "line": 3,
+                "column": 20
+            },
+            "labels": []
+        },
+        {
+            "text": ".L1:",
+            "source": null,
+            "labels": []
+        },
+        {
+            "text": "        retw.n",
+            "source": {
+                "file": null,
+                "line": 5,
+                "column": 1
+            },
+            "labels": []
+        }
+    ],
+    "cfg": {
+        "square(int, int*, int&):": {
+            "nodes": [
+                {
+                    "id": "square(int, int*, int&):",
+                    "label": "square(int, int*, int&):\n        entry   sp, 32\n        blti    a2, 1, .L1"
+                },
+                {
+                    "id": "square(int, int*, int&):@3",
+                    "label": "square(int, int*, int&):@3\n        slli    a8, a2, 2\n        addi    a8, a8, -4\n        srli    a8, a8, 2\n        addi.n  a8, a8, 1\n        loop    a8, .L3_LEND\n        l32i.n  a2, a3, 0\n        l32i.n  a9, a4, 0\n        add.n   a2, a2, a9\n        s32i.n  a2, a3, 0\n        addi.n  a3, a3, 4"
+                },
+                {
+                    "id": ".L1:",
+                    "label": ".L1:\n        retw.n"
+                }
+            ],
+            "edges": [
+                {
+                    "from": "square(int, int*, int&):",
+                    "to": ".L1:",
+                    "arrows": "to",
+                    "color": "green"
+                },
+                {
+                    "from": "square(int, int*, int&):",
+                    "to": "square(int, int*, int&):@3",
+                    "arrows": "to",
+                    "color": "red"
+                },
+                {
+                    "from": "square(int, int*, int&):@3",
+                    "to": ".L1:",
+                    "arrows": "to",
+                    "color": "grey"
+                }
+            ]
+        }
+    }
+}
\ No newline at end of file
diff --git a/test/cfg-tests.ts b/test/cfg-tests.ts
index 809994957..57bb35cf3 100644
--- a/test/cfg-tests.ts
+++ b/test/cfg-tests.ts
@@ -91,4 +91,20 @@ describe('Cfg test cases', () => {
             });
         }
     });
+
+    describe('xtensa', () => {
+        // instructionSet is a real value, group/version/compilerType just need to be distinct from others
+        const xtensaCompilerInfo = makeFakeCompilerInfo({
+            instructionSet: 'xtensa',
+            group: 'xtensa',
+            version: 'xtensa',
+            compilerType: 'xtensa',
+        });
+
+        for (const filename of files.filter(x => x.includes('xtensa'))) {
+            it(filename, async () => {
+                await DoCfgTest('python', path.join(testcasespath, filename), false, xtensaCompilerInfo);
+            });
+        }
+    });
 });