Repository: compiler-explorer/compiler-explorer
Pull request #7406 (2025-02-18 22:14:13)

Add version info for R8/D8
This fixes the version info button in the compiler window for R8/D8. Version info is pulled from the version properties file for latest, and from the filename for named releases.

<!-- THIS COMMENT IS INVISIBLE IN THE FINAL PR, BUT FEEL FREE TO REMOVE IT
Thanks for taking the time to improve CE. We really appreciate it.
  Before opening the PR, please make sure that the tests & linter pass their checks,
  by running `make check`.
  In the best case scenario, you are also adding tests to back up your changes,
  but don't sweat it if you don't. We can discuss them at a later date.
Feel free to append your name to the CONTRIBUTORS.md file
Thanks again, we really appreciate this!
-->


kevinjeon-g: This relies on https://github.com/compiler-explorer/infra/pull/1531 to be run in order to work properly for latest (thanks for the quick review, Matt!), but should be safe before that; the version will just say 'latest'.

Patch:
diff --git a/.github/labeler.yml b/.github/labeler.yml
index e147e9b68..a5935c35c 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -267,6 +267,12 @@
           - 'etc/config/nim.*.properties'
           - 'static/modes/nim-mode.ts'
 
+'lang-numba':
+  - changed-files:
+      - any-glob-to-any-file:
+          - 'lib/compilers/numba.ts'
+          - 'etc/config/numba.*.properties'
+
 'lang-objc':
   - changed-files:
       - any-glob-to-any-file:
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index db8e13501..2854e0f23 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -152,5 +152,6 @@ From oldest to newest contributor, we would like to thank:
 - [Pierre Bayerl](https://github.com/goto40)
 - [James Touton](https://github.com/Bekenn)
 - [Waqar Ahmed](https://github.com/waqar144)
+- [Vipul Cariappa](https://github.com/Vipul-Cariappa)
 - [Niles Salter](https://github.com/Validark)
 - [Stephen Heumann](https://github.com/sheumann)
diff --git a/etc/config/c++.amazon.properties b/etc/config/c++.amazon.properties
index 2282d9e9f..6721b3b87 100644
--- a/etc/config/c++.amazon.properties
+++ b/etc/config/c++.amazon.properties
@@ -1185,7 +1185,7 @@ compiler.hexagonclang1605.compilerCategories=clang-hexagon
 
 ################################
 # Clang for clad
-group.clad-clang.compilers=clang1810_clad180:clang1810_clad_trunk
+group.clad-clang.compilers=clang1810_clad180:clang1910_clad190:clad_trunk
 group.clad-clang.intelAsm=-mllvm --x86-asm-syntax=intel
 group.clad-clang.groupName=Clad Clang
 group.clad-clang.instructionSet=amd64
@@ -1202,11 +1202,17 @@ compiler.clang1810_clad180.name=clad v1.8 (clang 18.1.0)
 compiler.clang1810_clad180.options=--gcc-toolchain=/opt/compiler-explorer/gcc-13.2.0 -fplugin=/opt/compiler-explorer/clang-plugins/clad-1.8-clang-18.1.0/lib/clad.so -I/opt/compiler-explorer/clang-plugins/clad-1.8-clang-18.1.0/include
 compiler.clang1810_clad180.ldPath=${exePath}/../lib|${exePath}/../lib/x86_64-unknown-linux-gnu
 compiler.clang1810_clad180.debugPatched=true
-compiler.clang1810_clad_trunk.exe=/opt/compiler-explorer/clang-18.1.0/bin/clang++
-compiler.clang1810_clad_trunk.name=clad trunk (clang 18.1.0)
-compiler.clang1810_clad_trunk.options=--gcc-toolchain=/opt/compiler-explorer/gcc-13.2.0 -fplugin=/opt/compiler-explorer/clang-plugins/clad-trunk-clang-18.1.0/lib/clad.so -I/opt/compiler-explorer/clang-plugins/clad-trunk-clang-18.1.0/include
-compiler.clang1810_clad_trunk.ldPath=${exePath}/../lib|${exePath}/../lib/x86_64-unknown-linux-gnu
-compiler.clang1810_clad_trunk.debugPatched=true
+compiler.clang1910_clad190.exe=/opt/compiler-explorer/clang-19.1.0/bin/clang++
+compiler.clang1910_clad190.name=clad v1.9 (clang 19.1.0)
+compiler.clang1910_clad190.options=--gcc-toolchain=/opt/compiler-explorer/gcc-13.2.0 -fplugin=/opt/compiler-explorer/clang-plugins/clad-1.9-clang-19.1.0/lib/clad.so -I/opt/compiler-explorer/clang-plugins/clad-1.9-clang-19.1.0/include
+compiler.clang1910_clad190.ldPath=${exePath}/../lib|${exePath}/../lib/x86_64-unknown-linux-gnu
+compiler.clang1910_clad190.debugPatched=true
+compiler.clad_trunk.alias=clang1810_clad_trunk
+compiler.clad_trunk.exe=/opt/compiler-explorer/clang-19.1.0/bin/clang++
+compiler.clad_trunk.name=clad trunk (clang 19.1.0)
+compiler.clad_trunk.options=--gcc-toolchain=/opt/compiler-explorer/gcc-13.2.0 -fplugin=/opt/compiler-explorer/clang-plugins/clad-trunk-clang-19.1.0/lib/clad.so -I/opt/compiler-explorer/clang-plugins/clad-trunk-clang-19.1.0/include
+compiler.clad_trunk.ldPath=${exePath}/../lib|${exePath}/../lib/x86_64-unknown-linux-gnu
+compiler.clad_trunk.debugPatched=true
 
 ################################
 # Clang for Loongarch
diff --git a/etc/config/numba.amazon.properties b/etc/config/numba.amazon.properties
new file mode 100644
index 000000000..dc401a1e5
--- /dev/null
+++ b/etc/config/numba.amazon.properties
@@ -0,0 +1,8 @@
+compilers=&numba
+defaultCompiler=numba_0_61_0
+
+group.numba.compilers=numba_0_61_0
+group.numba.baseName=Numba
+
+compiler.numba_0_61_0.exe=/opt/compiler-explorer/numba/v0.61.0/bin/python3
+compiler.numba_0_61_0.semver=0.61.0
diff --git a/etc/config/numba.defaults.properties b/etc/config/numba.defaults.properties
new file mode 100644
index 000000000..05767c04c
--- /dev/null
+++ b/etc/config/numba.defaults.properties
@@ -0,0 +1,16 @@
+compilers=&numba
+defaultCompiler=numba_0_61_0
+
+group.numba.compilers=numba_0_61_0
+group.numba.baseName=Numba
+
+compiler.numba_0_61_0.exe=/path/to/python3.13
+compiler.numba_0_61_0.semver=0.61.0
+
+compilerType=numba
+demangler=c++filt
+interpreted=true
+isSemVer=true
+needsMulti=false
+supportsBinary=false
+supportsExecute=true
diff --git a/etc/scripts/numba_wrapper.py b/etc/scripts/numba_wrapper.py
new file mode 100644
index 000000000..3672be3bc
--- /dev/null
+++ b/etc/scripts/numba_wrapper.py
@@ -0,0 +1,124 @@
+# 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.
+from __future__ import annotations
+
+import argparse
+import contextlib
+import importlib.util
+import inspect
+import sys
+import traceback
+from typing import TYPE_CHECKING, TextIO
+
+from numba.core.dispatcher import Dispatcher
+
+if TYPE_CHECKING:
+    from collections.abc import Iterator
+    from types import ModuleType
+
+
+def main() -> None:
+    """Load a python module and write its public numba assembly to an output stream."""
+    parser = argparse.ArgumentParser(
+        description="Output compiled asm from public numba-compiled functions.",
+    )
+    parser.add_argument("--inputfile", required=True)
+    parser.add_argument("--outputfile")  # if not provided, we default to stdout
+    args = parser.parse_args()
+
+    with (
+        _handle_exceptions(),
+        _open_or_stdout(args.outputfile) as writer,
+    ):
+        _write_module_asm(path=args.inputfile, writer=writer)
+
+
+def _write_module_asm(*, path: str, writer: TextIO) -> None:
+    """Write assembly code from compiled Numba functions in the module at `path`.
+
+    - We only take code from public Numba Dispatchers in the module.
+    - We add comments to indicate source line numbers.
+
+    Args:
+        path: Target file path containing Python code to load as a module.
+        writer: Where we write the resulting code.
+    """
+    module = _load_module(path=path)
+    dispatchers = (
+        value
+        for name, value in inspect.getmembers(module)
+        # Leading underscore conventionally means private in Python culture.
+        # Numba manages compiled functions with Dispatcher objects.
+        if not name.startswith("_") and isinstance(value, Dispatcher)
+    )
+    # Multiple variables can reference the same dispatcher.
+    # We prefer source-ordered code for stable colors.
+    for dispatcher in sorted(set(dispatchers), key=_line_number):
+        for asm in dispatcher.inspect_asm().values():
+            assert isinstance(asm, str)  # for static type checkers
+            asm = _encode_line_number(asm, _line_number(dispatcher))
+            writer.write(asm)
+
+
+def _encode_line_number(asm: str, line_number: int) -> str:
+    # Numba doesn't natively encode line numbers. Appending them in comments
+    # at line endings makes them survive through all other asm pre-processing.
+    return asm.replace("\n", f";{line_number}\n")
+
+
+def _line_number(dispatcher: Dispatcher) -> int:
+    return dispatcher.py_func.__code__.co_firstlineno
+
+
+def _load_module(*, path: str, name: str = "example") -> ModuleType:
+    spec = importlib.util.spec_from_file_location(name, path)
+    assert spec is not None and spec.loader is not None  # for static type checkers
+    module = importlib.util.module_from_spec(spec)
+    spec.loader.exec_module(module)
+    return module
+
+
+@contextlib.contextmanager
+def _handle_exceptions() -> Iterator[None]:
+    try:
+        yield
+    except Exception as error:
+        # On error, we should we mimic the compiler-explorer Python wrapper's behavior
+        # in its traceback-hiding and exit code.
+        messages = traceback.format_exception_only(type(error), error)
+        sys.stderr.writelines(messages)
+        sys.exit(255)
+
+
+@contextlib.contextmanager
+def _open_or_stdout(maybe_path: str | None) -> Iterator[TextIO]:
+    if maybe_path is None:
+        yield sys.stdout
+        return
+    with open(maybe_path, "w", encoding="utf-8") as writer:
+        yield writer
+
+
+if __name__ == "__main__":
+    main()
diff --git a/examples/numba/default.py b/examples/numba/default.py
new file mode 100644
index 000000000..91250a086
--- /dev/null
+++ b/examples/numba/default.py
@@ -0,0 +1,6 @@
+import numba
+
+
+@numba.njit("int32(int32)")
+def square(num):
+    return num * num
diff --git a/examples/numba/generator.py b/examples/numba/generator.py
new file mode 100644
index 000000000..3cbecc45b
--- /dev/null
+++ b/examples/numba/generator.py
@@ -0,0 +1,19 @@
+import itertools
+
+import numba
+
+
+@numba.njit(locals={"x": numba.uint64})
+def xorshift(x):
+    while True:
+        x ^= x >> 13
+        x ^= x << 7
+        x ^= x >> 17
+        yield x
+
+
+RNG = xorshift(1)
+
+if __name__ == "__main__":
+    for x in itertools.islice(RNG, 16):
+        print(f"{x:016x}")
diff --git a/lib/base-compiler.ts b/lib/base-compiler.ts
index 63b8a826a..7c364fed1 100644
--- a/lib/base-compiler.ts
+++ b/lib/base-compiler.ts
@@ -140,6 +140,7 @@ import {
 } from './toolchain-utils.js';
 import type {ITool} from './tooling/base-tool.interface.js';
 import * as utils from './utils.js';
+import {resultLinesToText} from './utils.js';
 
 const compilationTimeHistogram = new PromClient.Histogram({
     name: 'ce_base_compiler_compilation_duration_seconds',
@@ -1486,6 +1487,14 @@ export class BaseCompiler {
         const output = await this.runCompiler(this.compiler.exe, newOptions, this.filename(inputFilename), execOptions);
         const compileEnd = performance.now();
 
+        if (output.code) {
+            return {
+                error: `Invocation failed: ${resultLinesToText(output.stderr)}${resultLinesToText(output.stdout)}}`,
+                results: {},
+                compileTime: output.execTime || compileEnd - compileStart,
+            };
+        }
+
         if (output.timedOut) {
             return {
                 error: 'Invocation timed out',
diff --git a/lib/compilers/_all.ts b/lib/compilers/_all.ts
index 230f87529..ffc8751f5 100644
--- a/lib/compilers/_all.ts
+++ b/lib/compilers/_all.ts
@@ -105,6 +105,7 @@ export {MrustcCompiler} from './mrustc.js';
 export {Msp430Compiler} from './msp430.js';
 export {NasmCompiler} from './nasm.js';
 export {NimCompiler} from './nim.js';
+export {NumbaCompiler} from './numba.js';
 export {NvccCompiler} from './nvcc.js';
 export {NvcppCompiler} from './nvcpp.js';
 export {NvrtcCompiler} from './nvrtc.js';
diff --git a/lib/compilers/d8.ts b/lib/compilers/d8.ts
index c0b09f8af..4fc42b63a 100644
--- a/lib/compilers/d8.ts
+++ b/lib/compilers/d8.ts
@@ -57,6 +57,9 @@ export class D8Compiler extends BaseCompiler implements SimpleOutputFilenameComp
     javaId: string;
     kotlinId: string;
 
+    versionFromPropsRegex: RegExp;
+    versionFromJarRegex: RegExp;
+
     libPaths: string[];
 
     constructor(compilerInfo: PreliminaryCompilerInfo, env: CompilationEnvironment) {
@@ -76,6 +79,9 @@ export class D8Compiler extends BaseCompiler implements SimpleOutputFilenameComp
             this.javaId = this.compilerProps<string>(`group.${this.compiler.group}.javaId`);
         }
 
+        this.versionFromPropsRegex = /^version\.version=(.*)$/;
+        this.versionFromJarRegex = /^.*r8-(.*)\.jar$/;
+
         this.kotlinId = this.compilerProps<string>(`group.${this.compiler.group}.kotlinId`);
 
         this.libPaths = [];
@@ -287,4 +293,25 @@ export class D8Compiler extends BaseCompiler implements SimpleOutputFilenameComp
         });
         return this.libPaths;
     }
+
+    override async getVersion() {
+        const versionFile = path.join(path.dirname(this.compiler.exe), 'r8-version.properties');
+        let versionCode;
+        if (await fs.exists(versionFile)) {
+            const versionInfo = await fs.readFile(versionFile, {encoding: 'utf8'});
+            for (const l of versionInfo.split(/\n/)) {
+                if (this.versionFromPropsRegex.test(l)) {
+                    versionCode = l.match(this.versionFromPropsRegex)![1];
+                }
+            }
+        } else {
+            // Non-latest R8 already has the version in the filename.
+            versionCode = this.compiler.exe.match(this.versionFromJarRegex)![1];
+        }
+        return {
+            stdout: versionCode,
+            stderr: '',
+            code: 0,
+        };
+    }
 }
diff --git a/lib/compilers/numba.ts b/lib/compilers/numba.ts
new file mode 100644
index 000000000..50fb6cd00
--- /dev/null
+++ b/lib/compilers/numba.ts
@@ -0,0 +1,96 @@
+// 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 type {PreliminaryCompilerInfo} from '../../types/compiler.interfaces.js';
+import type {ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces.js';
+import {BaseCompiler} from '../base-compiler.js';
+import {CompilationEnvironment} from '../compilation-env.js';
+import {AsmParser} from '../parsers/asm-parser.js';
+import {resolvePathFromAppRoot} from '../utils.js';
+
+import {BaseParser} from './argument-parsers.js';
+
+export class NumbaCompiler extends BaseCompiler {
+    private compilerWrapperPath: string;
+
+    static get key() {
+        return 'numba';
+    }
+
+    constructor(compilerInfo: PreliminaryCompilerInfo, env: CompilationEnvironment) {
+        super(compilerInfo, env);
+        this.compilerWrapperPath =
+            this.compilerProps('compilerWrapper', '') || resolvePathFromAppRoot('etc', 'scripts', 'numba_wrapper.py');
+    }
+
+    override async processAsm(result, filters, options) {
+        const processed = await super.processAsm(result, filters, options);
+        // Numba's function-end labels survive standard filtering.
+        if (filters.labels) {
+            processed.asm = processed.asm.filter(item => !item.text.startsWith('.Lfunc_end'));
+        }
+        if (!(this.asm instanceof AsmParser)) return processed;
+        for (const item of processed.asm) {
+            // We receive line numbers as comments to line ends.
+            const match = item.text.match(/;(\d+)$/);
+            if (!match) continue;
+            item.text = item.text.slice(0, match.index);
+            const inNvccCode = false;
+            if (this.asm.hasOpcode(item.text, inNvccCode)) item.source = {line: Number.parseInt(match[1]), file: null};
+        }
+        return processed;
+    }
+
+    override async postProcessAsm(result, filters?: ParseFiltersAndOutputOptions) {
+        result = await super.postProcessAsm(result, filters);
+        for (const item of result.asm) {
+            let line = item.text;
+            // Numba includes long and noisy abi tags.
+            line = line.replaceAll(/\[abi:\w+]/g, '');
+            // Numba's custom name mangling is not invertible. It escapes symbols to
+            // valid Python identifiers in a "_%02x" format. Because users can write
+            // coinciding identifiers, we cannot perfectly demangle. Python qualifies
+            // scoped function names with "<locals>". There is little risk from
+            // collisions with user-defined symbols including `_3clocals_3e`.
+            line = line.replaceAll('::_3clocals_3e::', '::<locals>::');
+            // Numba's generators have many escaped symbols in their argument listings.
+            line = line.replace(/::next\(\w+_20generator_28\w+\)/, decode_symbols);
+            item.text = line;
+        }
+        return result;
+    }
+
+    override optionsForFilter(filters: ParseFiltersAndOutputOptions, outputFilename: string): string[] {
+        return ['-I', this.compilerWrapperPath, '--outputfile', outputFilename, '--inputfile'];
+    }
+
+    override getArgumentParserClass() {
+        return BaseParser;
+    }
+}
+
+export function decode_symbols(text: string): string {
+    // Numba escapes /[^a-z0-9_]/ characters to "_%02x"-formatted strings.
+    return text.replaceAll(/_([\da-f]{2})/g, (_, hex) => String.fromCodePoint(Number.parseInt(hex, 16)));
+}
diff --git a/lib/compilers/orca.ts b/lib/compilers/orca.ts
index 88698f6bc..731d2575b 100644
--- a/lib/compilers/orca.ts
+++ b/lib/compilers/orca.ts
@@ -43,6 +43,7 @@ export class ORCACompiler extends BaseCompiler {
     constructor(compilerInfo: PreliminaryCompilerInfo, env: CompilationEnvironment) {
         super(compilerInfo, env);
         this.goldenGate = this.compilerProps<string>(`compiler.${this.compiler.id}.goldenGate`);
+        this.externalparser = null;
         this.asm = new ORCAAsmParser(this.compilerProps);
         this.compiler.demangler = '';
         this.demanglerClass = null;
diff --git a/lib/exec.ts b/lib/exec.ts
index cd40acc0e..186bb6d29 100644
--- a/lib/exec.ts
+++ b/lib/exec.ts
@@ -73,7 +73,7 @@ function setupOnError(stream: Stream, name: string) {
     });
 }
 
-async function maybeUnbuffer(command: string, args: string[]): Promise<string> {
+async function maybeUnbuffer(command: string, args: string[]): Promise<{command: string; args: string[]}> {
     if (!stdbufPath) {
         const unbufferStdoutExe = execProps<string>('unbufferStdoutExe');
         if (unbufferStdoutExe) {
@@ -85,11 +85,11 @@ async function maybeUnbuffer(command: string, args: string[]): Promise<string> {
 
     if (stdbufPath) {
         const stdbufArgs = splitArguments(execProps<string>('unbufferStdoutArgs'));
-        args.unshift(...stdbufArgs, command);
-        command = stdbufPath;
+        stdbufArgs.push(command);
+        logger.info(`Unbuffering ${command} with ${stdbufPath} ${stdbufArgs.join(' ')}`);
+        return {command: stdbufPath, args: stdbufArgs.concat([command], args)};
     }
-
-    return command;
+    return {command, args};
 }
 
 export async function executeDirect(
@@ -435,8 +435,8 @@ export async function sandbox(
     const dispatchEntry = sandboxDispatchTable[type as 'none' | 'nsjail' | 'firejail' | 'cewrapper'];
     if (!dispatchEntry) throw new Error(`Bad sandbox type ${type}`);
     if (!command) throw new Error('No executable provided');
-    command = await maybeUnbuffer(command, args);
-    return await dispatchEntry(command, args, options);
+    const unbuffered = await maybeUnbuffer(command, args);
+    return await dispatchEntry(unbuffered.command, unbuffered.args, options);
 }
 
 const wineSandboxName = 'ce-wineserver';
@@ -654,6 +654,6 @@ export async function execute(
     const dispatchEntry = executeDispatchTable[type];
     if (!dispatchEntry) throw new Error(`Bad sandbox type ${type}`);
     if (!command) throw new Error('No executable provided');
-    command = await maybeUnbuffer(command, args);
-    return await dispatchEntry(command, args, options);
+    const unbuffered = await maybeUnbuffer(command, args);
+    return await dispatchEntry(unbuffered.command, unbuffered.args, options);
 }
diff --git a/lib/languages.ts b/lib/languages.ts
index ff50d9867..96377174c 100644
--- a/lib/languages.ts
+++ b/lib/languages.ts
@@ -578,6 +578,17 @@ const definitions: Record<LanguageKey, LanguageDefinition> = {
         previewFilter: null,
         monacoDisassembly: null,
     },
+    numba: {
+        name: 'Numba',
+        monaco: 'python',
+        extensions: ['.py'],
+        alias: [],
+        logoUrl: 'numba.svg',
+        logoUrlDark: null,
+        formatter: null,
+        previewFilter: null,
+        monacoDisassembly: null,
+    },
     objc: {
         name: 'Objective-C',
         monaco: 'objective-c',
diff --git a/lib/utils.ts b/lib/utils.ts
index e8f5e6948..7e21e1bfc 100644
--- a/lib/utils.ts
+++ b/lib/utils.ts
@@ -592,3 +592,7 @@ export function deltaTimeNanoToMili(startTime: bigint, endTime: bigint): number
 export async function sleep(ms: number) {
     return new Promise(resolve => setTimeout(resolve, ms));
 }
+
+export function resultLinesToText(lines: ResultLine[]): string {
+    return lines.map(line => line.text).join('\n');
+}
diff --git a/types/languages.interfaces.ts b/types/languages.interfaces.ts
index 211432cdc..7a3bebca1 100644
--- a/types/languages.interfaces.ts
+++ b/types/languages.interfaces.ts
@@ -73,6 +73,7 @@ export type LanguageKey =
     | 'mlir'
     | 'modula2'
     | 'nim'
+    | 'numba'
     | 'ocaml'
     | 'odin'
     | 'objc'
diff --git a/views/resources/logos/numba.svg b/views/resources/logos/numba.svg
new file mode 100644
index 000000000..a0e0cbf70
--- /dev/null
+++ b/views/resources/logos/numba.svg
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generator: Adobe Illustrator 20.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 500 500" style="enable-background:new 0 0 500 500;" xml:space="preserve">
+<style type="text/css">
+	.st0{fill:#00A3E0;}
+	.st1{fill:none;}
+	.st2{fill:url(#SVGID_1_);}
+</style>
+<g>
+	<path class="st0" d="M333.2,310.3c0,0,136.3-100.5,81.1-165.5c-64-75.4-243.4,84.5-263,52S310.5,86.4,329.9,73.4   c2.1-1.4,2.7-4.1,2.1-7.2l20.7-2.4l-12.9-6.5l3.3-12.9l-15,11.7c-5.6-10.1-15.2-21-20.7-24.8c-19.5-12.9-55.2-9.8-107.1,16.2   S28,154.6,89.6,245.5c55.2,81.1,213.8-58.3,240.3-35.8c22.7,19.5-152.5,139.6-152.5,139.6s74.4,0,74.7,0   C239.2,381.8,177.4,476,177.4,476l224.8-162.4C378.6,310.1,333.2,310.3,333.2,310.3z M272.1,41c8-0.6,14.9,5.6,15.5,13.5   c0.6,8-5.6,14.9-13.5,15.5c-8,0.6-14.9-5.6-15.5-13.5C257.9,48.5,264.1,41.6,272.1,41z"/>
+</g>
+<rect x="22.5" y="22.5" class="st1" width="455" height="455"/>
+<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="301.6" y1="435.7" x2="301.6" y2="435.7" gradientTransform="matrix(1 0 0 -1 0 500)">
+	<stop offset="0" style="stop-color:#243746"/>
+	<stop offset="9.525055e-02" style="stop-color:#223442"/>
+	<stop offset="0.1897" style="stop-color:#1C2B36"/>
+	<stop offset="0.2839" style="stop-color:#121B22"/>
+	<stop offset="0.3773" style="stop-color:#030507"/>
+	<stop offset="0.3961" style="stop-color:#000000"/>
+</linearGradient>
+<path class="st2" d="M301.6,64.3"/>
+</svg>
\ No newline at end of file


Test patch:
diff --git a/etc/scripts/test_numba_wrapper.py b/etc/scripts/test_numba_wrapper.py
new file mode 100644
index 000000000..6e47fe499
--- /dev/null
+++ b/etc/scripts/test_numba_wrapper.py
@@ -0,0 +1,190 @@
+# 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.
+from __future__ import annotations
+
+import argparse
+import inspect
+import io
+import os
+import sys
+import unittest
+import unittest.mock
+
+import numba
+from numba.core.caching import tempfile
+
+from . import numba_wrapper
+
+
+class TestMain(unittest.TestCase):
+    def test_with_output_file(self) -> None:
+        source = "<source>"
+        with (
+            tempfile.NamedTemporaryFile() as output_file,
+            unittest.mock.patch.object(numba_wrapper, "_write_module_asm") as mock,
+            unittest.mock.patch.object(
+                argparse.ArgumentParser,
+                "parse_args",
+                return_value=argparse.Namespace(
+                    inputfile=source,
+                    outputfile=output_file.name,
+                ),
+            ),
+        ):
+            numba_wrapper.main()
+        mock.assert_called_once()
+        self.assertEqual(mock.call_args.kwargs["path"], source)
+        self.assertEqual(mock.call_args.kwargs["writer"].name, output_file.name)
+
+    def test_without_output_file(self) -> None:
+        with (
+            unittest.mock.patch.object(numba_wrapper, "_write_module_asm") as mock,
+            unittest.mock.patch.object(
+                argparse.ArgumentParser,
+                "parse_args",
+                return_value=argparse.Namespace(inputfile="test", outputfile=None),
+            ),
+        ):
+            numba_wrapper.main()
+        self.assertEqual(mock.call_args.kwargs["writer"], sys.stdout)
+
+
+class TestWriteModuleAsm(unittest.TestCase):
+    def test_asm(self) -> None:
+        # This test is slow, (~0.2s in local testing).
+        # Reducing the optimization level (NUMBA_OPT=0) made negligible difference.
+        # Adding the second compiled function gave only a small increase, suggesting
+        # that we suffer startup overhead.
+        source = (
+            "import numba\n"
+            "\n"
+            "\n"
+            "@numba.njit(numba.int32(numba.int32))\n"
+            "def square(num):\n"
+            "    return num * num\n"
+            "\n"
+            "\n"
+            "square_alias = square\n"
+            "\n"
+            "@numba.njit(numba.int32(numba.int32))\n"
+            "def cube(num):\n"
+            "    return num * num * num\n"
+        )
+        writer = io.StringIO()
+        with tempfile.TemporaryDirectory() as directory:
+            path = os.path.join(directory, "test_empty.py")
+            with open(path, "w") as file_:
+                file_.write(source)
+            numba_wrapper._write_module_asm(path=path, writer=writer)
+
+        asm = writer.getvalue()
+        self.assertIn("square", asm)
+        self.assertIn("cube", asm)
+        # Must be sorted by line number.
+        self.assertLess(asm.index("square"), asm.index("cube"))
+        # Aliasing `square` must not duplicate its code.
+        self.assertEqual(asm.count("square"), asm.count("cube"))
+
+    def test_no_error_on_no_dispatcher(self) -> None:
+        writer = io.StringIO()
+        numba_wrapper._write_module_asm(path=numba_wrapper.__file__, writer=writer)
+        self.assertEqual(writer.getvalue(), "")
+
+
+class TestLineNumber(unittest.TestCase):
+    def test_encode_line_number(self) -> None:
+        source = (
+            " push    rbp\n"
+            " mov     rbp, rsp\n"
+            " mov     DWORD PTR [rbp-4], edi\n"
+            " mov     eax, DWORD PTR [rbp-4]\n"
+            " imul    eax, eax\n"
+            " pop     rbp\n"
+            " ret\n"
+        )
+        line_number = 5678
+        source_commented = numba_wrapper._encode_line_number(source, line_number)
+
+        source_lines = source.split("\n")
+        result_lines = source_commented.split("\n")
+        self.assertEqual(len(source_lines), len(result_lines))
+        for before, after in zip(source_lines, result_lines, strict=True):
+            if before == "":
+                self.assertEqual(after, "")
+                continue
+            prefix, suffix = after.split(";")
+            self.assertEqual(before, prefix)
+            self.assertEqual(line_number, int(suffix))
+
+    def test_line_number(self) -> None:
+        def square(x):
+            return x * x
+
+        _, line_number = inspect.getsourcelines(square)
+        self.assertEqual(numba_wrapper._line_number(numba.njit(square)), line_number)
+
+
+class TestLoadModule(unittest.TestCase):
+    def test_simple(self) -> None:
+        name = "simple"
+        with tempfile.TemporaryDirectory() as directory:
+            path = os.path.join(directory, "test_simple.py")
+            with open(path, "w") as file_:
+                file_.write("x = 123")
+            module = numba_wrapper._load_module(path=file_.name, name=name)
+        self.assertEqual(module.__name__, name)
+        self.assertEqual(module.x, 123)
+
+
+class TestHandleExceptions(unittest.TestCase):
+    def test_no_exception(self) -> None:
+        with numba_wrapper._handle_exceptions() as nil:
+            self.assertIsNone(nil)
+
+    def test_exception(self) -> None:
+        text = "dQw4w9WgXcQ"
+        stderr = io.StringIO()
+        with (
+            self.assertRaisesRegex(SystemExit, "255"),
+            unittest.mock.patch.object(sys, "stderr", stderr),
+            numba_wrapper._handle_exceptions(),
+        ):
+            assert not text, text  # Just to raise an exception
+        self.assertIn(text, stderr.getvalue())
+
+
+class TestOpenOrStdout(unittest.TestCase):
+    def test_open(self) -> None:
+        text = "lea"
+        with tempfile.TemporaryDirectory() as directory:
+            filename = os.path.join(directory, "test_open.txt")
+            with numba_wrapper._open_or_stdout(filename) as file_:
+                self.assertIsNot(file_, sys.stdout)
+                file_.write(text)
+            with open(filename) as file_:
+                self.assertEqual(file_.read(), text)
+
+    def test_stdout(self) -> None:
+        with numba_wrapper._open_or_stdout(None) as file_:
+            self.assertIs(file_, sys.stdout)
diff --git a/test/numba-tests.ts b/test/numba-tests.ts
new file mode 100644
index 000000000..b71204ba4
--- /dev/null
+++ b/test/numba-tests.ts
@@ -0,0 +1,161 @@
+// 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 {beforeAll, describe, expect, it} from 'vitest';
+
+import type {CompilationEnvironment} from '../lib/compilation-env.js';
+import {BaseParser} from '../lib/compilers/argument-parsers.js';
+import {NumbaCompiler, decode_symbols} from '../lib/compilers/numba.js';
+import type {AsmResultSource} from '../types/asmresult/asmresult.interfaces.js';
+import type {LanguageKey} from '../types/languages.interfaces.js';
+
+import {makeCompilationEnvironment, makeFakeCompilerInfo} from './utils.js';
+
+describe('Numba', () => {
+    let ce: CompilationEnvironment;
+    const languages = {
+        numba: {id: 'numba' as LanguageKey},
+    };
+    const info = {
+        exe: 'none',
+        lang: languages.numba.id,
+    };
+    const filters = {
+        binary: false,
+        execute: false,
+        demangle: true,
+        intel: true,
+        commentOnly: true,
+        directives: true,
+        labels: true,
+        optOutput: false,
+        libraryCode: false,
+        trim: true,
+        binaryObject: false,
+        debugCalls: false,
+    };
+    const options = [];
+
+    beforeAll(() => {
+        ce = makeCompilationEnvironment({languages});
+    });
+
+    it('should quack like a numba compiler', () => {
+        const compiler = new NumbaCompiler(makeFakeCompilerInfo(info), ce);
+        expect(compiler.getArgumentParserClass()).toBe(BaseParser);
+        expect(NumbaCompiler.key).toEqual(languages.numba.id);
+    });
+
+    it('should give good wrapper script arguments', () => {
+        const compiler = new NumbaCompiler(makeFakeCompilerInfo(info), ce);
+        const outputFilename = 'test.log';
+        const options = compiler.optionsForFilter({}, outputFilename);
+        expect(options[0]).toEqual('-I');
+        expect(options[1]).toContain('numba_wrapper.py');
+        const i_outputfile = options.indexOf('--outputfile');
+        expect(i_outputfile).not.toEqual(-1);
+        expect(options[i_outputfile + 1]).toEqual(outputFilename);
+        expect(options.at(-1)!).toEqual('--inputfile');
+    });
+
+    it('processing should filter and add line numbers', async () => {
+        const compiler = new NumbaCompiler(makeFakeCompilerInfo(info), ce);
+        const asm =
+            ' .text;123\n' +
+            ' .file   "<string>";123\n' +
+            ' .globl  _ZNmangledEdd;123\n' +
+            ' .p2align        4, 0x90;123\n' +
+            ' .type   _ZNmangledEdd,@function;123\n' +
+            '_ZNmangledEdd:;123\n' +
+            '  pushq   %rbx;123\n' +
+            '  movq    %rdi, %rbx;123\n' +
+            '  movabsq $pow, %rax;123\n' +
+            '  callq   *%rax;123\n' +
+            '  vmovsd  %xmm0, (%rbx);123\n' +
+            '  xorl    %eax, %eax;123\n' +
+            '  popq    %rbx;123\n' +
+            '  retq;123\n' +
+            '.Lfunc_end0:;123\n' +
+            ' .size   _ZNmangledEdd, .Lfunc_end0-_ZNmangledEdd;123\n';
+        const processed = await compiler.processAsm({asm}, filters, options);
+        expect(processed.asm.map(item => item.text)).toEqual([
+            '_ZNmangledEdd:',
+            '  pushq %rbx',
+            '  movq %rdi, %rbx',
+            '  movabsq $pow, %rax',
+            '  callq *%rax',
+            '  vmovsd %xmm0, (%rbx)',
+            '  xorl %eax, %eax',
+            '  popq %rbx',
+            '  retq',
+        ]);
+        expect(processed.asm[0].source).toBeNull();
+        for (const item of processed.asm.slice(1)) {
+            expect(item.source).not.toBeNull();
+            expect(item.source).not.toBeUndefined();
+            const source = item.source as AsmResultSource;
+            expect(source.line).not.toBeNull();
+            expect(source.line).not.toBeUndefined();
+            const line = source.line as number;
+            expect(line).toEqual(123);
+        }
+    });
+
+    it('should demangle special strings', async () => {
+        const compiler = new NumbaCompiler(makeFakeCompilerInfo(info), ce);
+        const asm = [
+            // These text strings were pre-processed by the default demangler.
+            {
+                text:
+                    'example::factory::_3clocals_3e::power[abi:v1][abi:c8tJTIcFKzyF2ILShI4CrgQElQb6HczSBAA_3d]' +
+                    '(double, double):',
+            },
+            {
+                text:
+                    'example::xorshift64::next(' +
+                    'uint64_20generator_28func_3d_3cfunction_20xorshift64_20at_200x7fd5948c18a0_3e_2c_20args_3d' +
+                    '_28int64_2c_29_2c_20has_finalizer_3dTrue_29):',
+            },
+            {
+                text: 'example::square(Array<double, 1, C, mutable, aligned>):',
+            },
+        ];
+        const processed = await compiler.postProcessAsm({asm}, filters);
+        expect(processed.asm.map(item => item.text)).toEqual([
+            'example::factory::<locals>::power(double, double):',
+            'example::xorshift64::next(uint64 generator(func=<function xorshift64 at 0x7fd5948c18a0>, args=(int64,), ' +
+                'has_finalizer=True)):',
+            'example::square(Array<double, 1, C, mutable, aligned>):',
+        ]);
+    });
+
+    it('should invert the encoding accurately', () => {
+        expect(decode_symbols('plain_name123')).toEqual('plain_name123');
+        expect(decode_symbols('_3clocals_3e')).toEqual('<locals>');
+        const all_ascii = [...Array.from({length: 128}).keys()].map(i => String.fromCodePoint(i)).join('');
+        const encode = (c: string) => '_' + c.codePointAt(0)!.toString(16).padStart(2, '0');
+        const all_ascii_encoded = all_ascii.replaceAll(/[^\d_a-z]/g, encode);
+        expect(decode_symbols(all_ascii_encoded)).toEqual(all_ascii);
+    });
+});