mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2025-12-27 09:23:52 -05:00
Add Numba (#5592)
- Add initial support for Numba compilation: asm, demangling, execution Numba wraps Python functions in `Dispatcher` objects. Each dispatcher contains zero or more compiled argument-type-indexed overloads of its function. We import the user's code as a module, and emit the code from all overloads of all dispatchers that the module publicly exposes. Name mangling is odd in Numba. It uses a similar mangling syntax to C++, but also encodes non-symbol (`r"[^a-z0-9_]"`) characters as `_%02x`. This encoding yields valid identifier names, so it is not strictly invertible. Here, I have hard-coded some replacements to decode some common cases at the cost of possible clashes with ugly user-defined names. Screenshot captured via `make dev EXTRA_ARGS="--debug --language numba"`  ## To do - [x] Answer questions of #5591. - [x] Acquire a python environment. - [ ] Automatically run the python test? Locally, I have installed a virtual environment with python 3.12.3 with ```shell path/to/python -m venv venv_numba path/to/venv_numba/bin/python -m pip install numba==0.61.0 scipy>=0.16 ``` and configured its use with ```properties # compiler-explorer/etc/config/numba.local.properties 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 ``` I run this python-side test file with ```python path/to/venv_numba/bin/python -m unittest etc/scripts/test_numba_wrapper.py ``` --------- Co-authored-by: Mats Jun Larsen <mats@jun.codes>
This commit is contained in:
6
.github/labeler.yml
vendored
6
.github/labeler.yml
vendored
@@ -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:
|
||||
|
||||
8
etc/config/numba.amazon.properties
Normal file
8
etc/config/numba.amazon.properties
Normal file
@@ -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
|
||||
16
etc/config/numba.defaults.properties
Normal file
16
etc/config/numba.defaults.properties
Normal file
@@ -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
|
||||
124
etc/scripts/numba_wrapper.py
Normal file
124
etc/scripts/numba_wrapper.py
Normal file
@@ -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()
|
||||
190
etc/scripts/test_numba_wrapper.py
Normal file
190
etc/scripts/test_numba_wrapper.py
Normal file
@@ -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)
|
||||
6
examples/numba/default.py
Normal file
6
examples/numba/default.py
Normal file
@@ -0,0 +1,6 @@
|
||||
import numba
|
||||
|
||||
|
||||
@numba.njit("int32(int32)")
|
||||
def square(num):
|
||||
return num * num
|
||||
19
examples/numba/generator.py
Normal file
19
examples/numba/generator.py
Normal file
@@ -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}")
|
||||
@@ -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';
|
||||
|
||||
96
lib/compilers/numba.ts
Normal file
96
lib/compilers/numba.ts
Normal file
@@ -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)));
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
161
test/numba-tests.ts
Normal file
161
test/numba-tests.ts
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -73,6 +73,7 @@ export type LanguageKey =
|
||||
| 'mlir'
|
||||
| 'modula2'
|
||||
| 'nim'
|
||||
| 'numba'
|
||||
| 'ocaml'
|
||||
| 'odin'
|
||||
| 'objc'
|
||||
|
||||
22
views/resources/logos/numba.svg
Normal file
22
views/resources/logos/numba.svg
Normal file
@@ -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>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
Reference in New Issue
Block a user