diff --git a/mypy/typeshed/stubs/librt/librt/strings.pyi b/mypy/typeshed/stubs/librt/librt/strings.pyi index 215fdf6fd56b..5ab1e978d465 100644 --- a/mypy/typeshed/stubs/librt/librt/strings.pyi +++ b/mypy/typeshed/stubs/librt/librt/strings.pyi @@ -45,3 +45,4 @@ def read_f64_be(b: bytes, index: i64, /) -> float: ... # obtained via ord(s[i])). Negative inputs return False. def isspace(c: i32, /) -> bool: ... def isdigit(c: i32, /) -> bool: ... +def isalnum(c: i32, /) -> bool: ... diff --git a/mypyc/lib-rt/codepoint_extra_ops.h b/mypyc/lib-rt/codepoint_extra_ops.h index 13e530e1d90f..a4f4c6880caf 100644 --- a/mypyc/lib-rt/codepoint_extra_ops.h +++ b/mypyc/lib-rt/codepoint_extra_ops.h @@ -17,4 +17,8 @@ static inline bool LibRTStrings_IsDigit(int32_t c) { return c >= 0 && Py_UNICODE_ISDIGIT((Py_UCS4)c); } +static inline bool LibRTStrings_IsAlnum(int32_t c) { + return c >= 0 && Py_UNICODE_ISALNUM((Py_UCS4)c); +} + #endif // MYPYC_CODEPOINT_EXTRA_OPS_H diff --git a/mypyc/lib-rt/strings/librt_strings.c b/mypyc/lib-rt/strings/librt_strings.c index 97a1c67f4623..ce15107f7e09 100644 --- a/mypyc/lib-rt/strings/librt_strings.c +++ b/mypyc/lib-rt/strings/librt_strings.c @@ -1192,6 +1192,7 @@ cp_parse_i32(PyObject *arg, int32_t *out) { DEFINE_CP_BOOL_WRAPPER(isspace, LibRTStrings_IsSpace) DEFINE_CP_BOOL_WRAPPER(isdigit, LibRTStrings_IsDigit) +DEFINE_CP_BOOL_WRAPPER(isalnum, LibRTStrings_IsAlnum) static PyMethodDef librt_strings_module_methods[] = { {"write_i16_le", (PyCFunction) write_i16_le, METH_FASTCALL, @@ -1260,6 +1261,9 @@ static PyMethodDef librt_strings_module_methods[] = { {"isdigit", cp_isdigit, METH_O, PyDoc_STR("Test whether a codepoint (i32) is a Unicode digit.") }, + {"isalnum", cp_isalnum, METH_O, + PyDoc_STR("Test whether a codepoint (i32) is alphanumeric.") + }, {NULL, NULL, 0, NULL} }; diff --git a/mypyc/primitives/librt_strings_ops.py b/mypyc/primitives/librt_strings_ops.py index 968aeb6014c4..0432cade7b9a 100644 --- a/mypyc/primitives/librt_strings_ops.py +++ b/mypyc/primitives/librt_strings_ops.py @@ -413,3 +413,12 @@ error_kind=ERR_NEVER, dependencies=[LIBRT_STRINGS, CODEPOINT_EXTRA_OPS], ) + +function_op( + name="librt.strings.isalnum", + arg_types=[int32_rprimitive], + return_type=bool_rprimitive, + c_function_name="LibRTStrings_IsAlnum", + error_kind=ERR_NEVER, + dependencies=[LIBRT_STRINGS, CODEPOINT_EXTRA_OPS], +) diff --git a/mypyc/test-data/irbuild-librt-strings.test b/mypyc/test-data/irbuild-librt-strings.test index bffa96dd5009..8b27f6a67256 100644 --- a/mypyc/test-data/irbuild-librt-strings.test +++ b/mypyc/test-data/irbuild-librt-strings.test @@ -359,3 +359,17 @@ def is_d(c): L0: r0 = LibRTStrings_IsDigit(c) return r0 + +[case testLibrtStringsIsAlnumIR] +from librt.strings import isalnum +from mypy_extensions import i32 + +def is_an(c: i32) -> bool: + return isalnum(c) +[out] +def is_an(c): + c :: i32 + r0 :: bool +L0: + r0 = LibRTStrings_IsAlnum(c) + return r0 diff --git a/mypyc/test-data/run-librt-strings.test b/mypyc/test-data/run-librt-strings.test index 211f88f72e08..141d830d3d0b 100644 --- a/mypyc/test-data/run-librt-strings.test +++ b/mypyc/test-data/run-librt-strings.test @@ -1443,7 +1443,9 @@ def test_new_without_init_is_usable() -> None: [case testLibrtStringsCodepointClassifiers_librt] from typing import Any from mypy_extensions import i32 -from librt.strings import isspace, isdigit +from librt.strings import isspace, isdigit, isalnum + +from testutil import assertRaises def test_codepoint_classifiers() -> None: @@ -1451,6 +1453,7 @@ def test_codepoint_classifiers() -> None: for bad in (i32(-1), i32(-113)): assert not isspace(bad) assert not isdigit(bad) + assert not isalnum(bad) # Verify each codepoint primitive agrees with the matching str method # across all Unicode codepoints, including the ord(chr(i)) round-trip. # Any forces generic dispatch on the str side. @@ -1460,3 +1463,24 @@ def test_codepoint_classifiers() -> None: o = ord(c) assert isspace(o) == isspace(i) == a.isspace() assert isdigit(o) == isdigit(i) == a.isdigit() + assert isalnum(o) == isalnum(i) == a.isalnum() + + +def test_codepoint_classifiers_via_any() -> None: + # Iterate so the callee is opaque to mypyc and dispatch falls back to + # the PyMethodDef wrapper, exercising the i32 range check. + for fn, true_input, false_input in ( + (isspace, " ", "a"), + (isdigit, "5", "a"), + (isalnum, "A", " "), + ): + f: Any = fn + assert f(ord(true_input)) is True + assert f(ord(false_input)) is False + # Negative values are valid i32, just not codepoints. + assert f(-1) is False + # Inputs outside i32 range raise OverflowError through the wrapper. + with assertRaises(OverflowError, "codepoint out of i32 range"): + f(1 << 40) + with assertRaises(OverflowError, "codepoint out of i32 range"): + f(-(1 << 40))