Skip to content

Commit cd68999

Browse files
committed
Fix CABI calling convention: skip env global and escape mangled names
The CUDACABICallConv does not use a Numba environment for error-status propagation, yet the lowering unconditionally emitted a NumbaEnv global into every module. For lambdas this also produced invalid NVVM IR because the CABI mangler passed raw `<lambda>` (with angle brackets) into the global name, causing ERROR_INVALID_IR at verification. Changes: - Add `needs_env` class attribute to BaseCallConv (True) and override it as False in CUDACABICallConv. Guard `emit_environment_object()` in `BaseLower.lower()` behind this flag. - Fix `CUDACABICallConv.mangler` to run identifiers through `itanium_mangler.escape_string()` so characters like `<` and `>` are hex-escaped (`_3c`, `_3e`). - Promote `_escape_string` to public API `escape_string` (with a backward-compatible alias) and improve its docstring. - Add tests for `escape_string` and for CABI lambda compilation / env omission. Made-with: Cursor
1 parent 05922fc commit cd68999

5 files changed

Lines changed: 103 additions & 12 deletions

File tree

numba_cuda/numba/cuda/core/callconv.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ def _const_int(code):
5353

5454

5555
class BaseCallConv:
56+
needs_env = True
57+
5658
def __init__(self, context):
5759
self.context = context
5860

@@ -398,8 +400,13 @@ class CUDACABICallConv(BaseCallConv):
398400
<Python return type> (<Python arguments>)
399401
400402
Exceptions are unsupported in this convention.
403+
404+
No Numba environment pointer is needed because there is no error-status
405+
channel to propagate exceptions through.
401406
"""
402407

408+
needs_env = False
409+
403410
def _make_call_helper(self, builder):
404411
# Call helpers are used to help report exceptions back to Python, so
405412
# none is required here.
@@ -501,10 +508,10 @@ def get_return_type(self, ty):
501508
return self.context.data_model_manager[ty].get_return_type()
502509

503510
def mangler(self, name, argtypes, *, abi_tags=None, uid=None):
511+
escaped = itanium_mangler.escape_string(name.split(".")[-1])
504512
if name.startswith(".NumbaEnv."):
505-
func_name = name.split(".")[-1]
506-
return f"_ZN08NumbaEnv{func_name}"
507-
return name.split(".")[-1]
513+
return f"_ZN08NumbaEnv{escaped}"
514+
return escaped
508515

509516

510517
class ErrorModel:

numba_cuda/numba/cuda/itanium_mangler.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,27 +62,34 @@
6262
}
6363

6464

65-
def _escape_string(text):
65+
def escape_string(text):
6666
"""Escape the given string so that it only contains ASCII characters
67-
of [a-zA-Z0-9_$].
67+
valid in mangled names: ``[a-zA-Z0-9_]``.
6868
69-
The dollar symbol ($) and other invalid characters are escaped into
70-
the string sequence of "$xx" where "xx" is the hex codepoint of the char.
69+
Invalid characters are hex-escaped as ``_xx`` where *xx* is the two-digit
70+
hex code point. Multibyte (non-ASCII) characters are first encoded to
71+
UTF-8 and each byte is escaped individually.
7172
72-
Multibyte characters are encoded into utf8 and converted into the above
73-
hex format.
73+
Examples::
74+
75+
>>> escape_string("hello")
76+
'hello'
77+
>>> escape_string("<lambda>")
78+
'_3clambda_3e'
7479
"""
7580

7681
def repl(m):
7782
return "".join(("_%02x" % ch) for ch in m.group(0).encode("utf8"))
7883

7984
ret = re.sub(_re_invalid_char, repl, text)
80-
# Return str if we got a unicode (for py2)
8185
if not isinstance(ret, str):
8286
return ret.encode("ascii")
8387
return ret
8488

8589

90+
_escape_string = escape_string
91+
92+
8693
def _fix_lead_digit(text):
8794
"""
8895
Fix text with leading digit

numba_cuda/numba/cuda/lowering.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,8 +228,10 @@ def emit_environment_object(self):
228228
self.context.declare_env_global(self.module, envname)
229229

230230
def lower(self):
231-
# Emit the Env into the module
232-
self.emit_environment_object()
231+
# Emit the Env into the module (only needed for calling conventions
232+
# that use a Numba environment for error-status propagation).
233+
if self.call_conv.needs_env:
234+
self.emit_environment_object()
233235
if self.generator_info is None:
234236
self.genlower = None
235237
self.lower_normal_function(self.fndesc)

numba_cuda/numba/cuda/tests/core/test_itanium_mangler.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,32 @@
88
import unittest
99

1010

11+
class TestEscapeString(unittest.TestCase):
12+
def test_plain_ascii(self):
13+
self.assertEqual(itanium_mangler.escape_string("hello"), "hello")
14+
15+
def test_underscores_and_digits(self):
16+
self.assertEqual(
17+
itanium_mangler.escape_string("my_func_2"), "my_func_2"
18+
)
19+
20+
def test_angle_brackets(self):
21+
self.assertEqual(
22+
itanium_mangler.escape_string("<lambda>"), "_3clambda_3e"
23+
)
24+
25+
def test_dot(self):
26+
self.assertEqual(itanium_mangler.escape_string("a.b"), "a_2eb")
27+
28+
def test_empty_string(self):
29+
self.assertEqual(itanium_mangler.escape_string(""), "")
30+
31+
def test_backward_compat_alias(self):
32+
self.assertIs(
33+
itanium_mangler._escape_string, itanium_mangler.escape_string
34+
)
35+
36+
1137
class TestItaniumManager(unittest.TestCase):
1238
def test_ident(self):
1339
got = itanium_mangler.mangle_identifier("apple")

numba_cuda/numba/cuda/tests/cudapy/test_compiler.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,55 @@ def _test_c_abi_with_abi_name(self, compile_function, default_kwargs):
449449
r"func_retval0\)\s+_Z4funcii\(",
450450
)
451451

452+
def test_c_abi_lambda_with_abi_name(self):
453+
"""Compiling a lambda with CABI + abi_name should produce valid IR.
454+
455+
Lambdas have ``<lambda>`` as their qualname; the angle brackets must be
456+
escaped when mangling names for NVVM.
457+
"""
458+
abi_info = {"abi_name": "blah"}
459+
460+
with self.subTest("compile_ptx"):
461+
self._test_c_abi_lambda_with_abi_name(
462+
compile_ptx,
463+
{"device": True, "abi": "c", "abi_info": abi_info},
464+
)
465+
466+
with self.subTest("compile_all"):
467+
self._test_c_abi_lambda_with_abi_name(
468+
compile_all,
469+
{
470+
"device": True,
471+
"abi": "c",
472+
"abi_info": abi_info,
473+
"output": "ptx",
474+
},
475+
)
476+
477+
def _test_c_abi_lambda_with_abi_name(
478+
self, compile_function, default_kwargs
479+
):
480+
ret = compile_function(lambda x: x, int32(int32), **default_kwargs)
481+
ptx, resty = self._handle_compile_result(ret, compile_function)
482+
483+
self.assertRegex(
484+
ptx,
485+
r"\.visible\s+\.func\s+\(\.param\s+\.b32\s+"
486+
r"func_retval0\)\s+blah\(",
487+
)
488+
489+
def test_c_abi_no_env_global(self):
490+
"""CABI functions should not emit a NumbaEnv global."""
491+
ret = compile_ptx(lambda x: x, int32(int32), device=True, abi="c")
492+
ptx, _ = self._handle_compile_result(ret, compile_ptx)
493+
self.assertNotIn("NumbaEnv", ptx)
494+
495+
def test_numba_abi_has_env_global(self):
496+
"""Numba-ABI functions still require a NumbaEnv global."""
497+
ret = compile_ptx(lambda x: x, int32(int32), device=True, abi="numba")
498+
ptx, _ = self._handle_compile_result(ret, compile_ptx)
499+
self.assertIn("NumbaEnv", ptx)
500+
452501
def test_c_abi_boolean_return(self):
453502
"""
454503
Tests that returning a raw boolean comparison (a == b) compiles correctly

0 commit comments

Comments
 (0)