Skip to content

Commit d7f7036

Browse files
[mypyc] Add librt.strings.isspace char primitive (#21462)
This PR serves as the foundation for the smaller alternative of the [`char` proposal request](#21418). It builds on top of the existing `librt.strings` and `ord(char)` specialization, so code like the following can lower to a direct codepoint check i.e without materializing a 1-character str: ```py from librt.strings import isspace from mypy_extensions import i32 c: i32 = ... if (isspace(c)): ... ``` <br /> Semantics: - Input type is `i32` - Negative inputs return `False` - For all valid Unicode codepoints, behavior matches `str.isspace()` on the corresponding 1-character string (ensured by exhaustive test too) This PR adds only `isspace`; It does not add any new type-system surface, and it does not introduce the rest of the codepoint helperfamily. I'll be contributing these next if this direction looks good. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 9b4e31c commit d7f7036

8 files changed

Lines changed: 181 additions & 1 deletion

File tree

mypy/typeshed/stubs/librt/librt/strings.pyi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,7 @@ def write_f64_le(b: BytesWriter, n: float, /) -> None: ...
4040
def write_f64_be(b: BytesWriter, n: float, /) -> None: ...
4141
def read_f64_le(b: bytes, index: i64, /) -> float: ...
4242
def read_f64_be(b: bytes, index: i64, /) -> float: ...
43+
44+
# Codepoint classification helpers operating on i32 codepoints (typically
45+
# obtained via ord(s[i])). Negative inputs return False.
46+
def isspace(c: i32, /) -> bool: ...

mypyc/ir/deps.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,5 @@ def get_header(self) -> str:
116116
STRING_WRITER_EXTRA_OPS: Final = SourceDep("stringwriter_extra_ops.c")
117117
BYTEARRAY_EXTRA_OPS: Final = SourceDep("bytearray_extra_ops.c")
118118
STR_EXTRA_OPS: Final = SourceDep("str_extra_ops.c")
119+
CODEPOINT_EXTRA_OPS: Final = SourceDep("codepoint_extra_ops.c")
119120
VECS_EXTRA_OPS: Final = SourceDep("vecs_extra_ops.c")

mypyc/lib-rt/codepoint_extra_ops.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#include "codepoint_extra_ops.h"
2+
3+
// Out-of-line bodies for codepoint helpers that are too large to inline.
4+
// The classification helpers and the ASCII fast paths for case conversion
5+
// stay inline in codepoint_extra_ops.h; this file holds the slow paths
6+
// that round-trip through PyUnicode_FromOrdinal and CPython's Unicode
7+
// machinery. Currently empty; populated as later commits add
8+
// isidentifier, toupper, and tolower.

mypyc/lib-rt/codepoint_extra_ops.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#ifndef MYPYC_CODEPOINT_EXTRA_OPS_H
2+
#define MYPYC_CODEPOINT_EXTRA_OPS_H
3+
4+
#include <Python.h>
5+
#include <stdbool.h>
6+
#include <stdint.h>
7+
8+
// Codepoint helpers for librt.strings.
9+
// Inputs are signed int32_t for compatibility with mypyc's i32 type.
10+
// Negative values are treated as non-codepoints and return false.
11+
12+
static inline bool LibRTStrings_IsSpace(int32_t c) {
13+
return c >= 0 && Py_UNICODE_ISSPACE((Py_UCS4)c);
14+
}
15+
16+
#endif // MYPYC_CODEPOINT_EXTRA_OPS_H

mypyc/lib-rt/strings/librt_strings.c

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include <Python.h>
55
#include <stdint.h>
66
#include "CPy.h"
7+
#include "codepoint_extra_ops.h"
78
#include "librt_strings.h"
89

910
#define CPY_BOOL_ERROR 2
@@ -1153,6 +1154,44 @@ read_f64_be(PyObject *module, PyObject *const *args, size_t nargs) {
11531154
return PyFloat_FromDouble(CPyBytes_ReadF64BEUnsafe(data + index));
11541155
}
11551156

1157+
// Codepoint classification helpers exposed to interpreted callers.
1158+
// The C-side names are prefixed `cp_` to avoid colliding with libc's
1159+
// <ctype.h> isspace / isdigit / etc. Compiled callers go through the
1160+
// LibRTStrings_* static inlines in codepoint_extra_ops.h instead.
1161+
//
1162+
// All wrappers parse a single int argument as i32 (codepoint) and
1163+
// dispatch to the corresponding LibRTStrings_* function. The parse
1164+
// step accepts any int but rejects values outside the i32 range with
1165+
// OverflowError, matching the input domain of the compiled fast path.
1166+
1167+
// Parse a Python int as i32 codepoint. Returns 0 on success and writes
1168+
// the value to *out; returns -1 on error with a Python exception set.
1169+
static int
1170+
cp_parse_i32(PyObject *arg, int32_t *out) {
1171+
int overflow;
1172+
long c = PyLong_AsLongAndOverflow(arg, &overflow);
1173+
if (c == -1 && PyErr_Occurred())
1174+
return -1;
1175+
if (overflow != 0 || c < INT32_MIN || c > INT32_MAX) {
1176+
PyErr_SetString(PyExc_OverflowError,
1177+
"codepoint out of i32 range");
1178+
return -1;
1179+
}
1180+
*out = (int32_t)c;
1181+
return 0;
1182+
}
1183+
1184+
#define DEFINE_CP_BOOL_WRAPPER(name, fn) \
1185+
static PyObject* \
1186+
cp_##name(PyObject *module, PyObject *arg) { \
1187+
int32_t c; \
1188+
if (cp_parse_i32(arg, &c) < 0) \
1189+
return NULL; \
1190+
return PyBool_FromLong(fn(c)); \
1191+
}
1192+
1193+
DEFINE_CP_BOOL_WRAPPER(isspace, LibRTStrings_IsSpace)
1194+
11561195
static PyMethodDef librt_strings_module_methods[] = {
11571196
{"write_i16_le", (PyCFunction) write_i16_le, METH_FASTCALL,
11581197
PyDoc_STR("Write a 16-bit signed integer to BytesWriter in little-endian format")
@@ -1214,6 +1253,9 @@ static PyMethodDef librt_strings_module_methods[] = {
12141253
{"read_f64_be", (PyCFunction) read_f64_be, METH_FASTCALL,
12151254
PyDoc_STR("Read a 64-bit float from bytes in big-endian format")
12161255
},
1256+
{"isspace", cp_isspace, METH_O,
1257+
PyDoc_STR("Test whether a codepoint (i32) is Unicode whitespace.")
1258+
},
12171259
{NULL, NULL, 0, NULL}
12181260
};
12191261

mypyc/primitives/librt_strings_ops.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
from mypyc.ir.deps import BYTES_WRITER_EXTRA_OPS, LIBRT_STRINGS, STRING_WRITER_EXTRA_OPS
1+
from mypyc.ir.deps import (
2+
BYTES_WRITER_EXTRA_OPS,
3+
CODEPOINT_EXTRA_OPS,
4+
LIBRT_STRINGS,
5+
STRING_WRITER_EXTRA_OPS,
6+
)
27
from mypyc.ir.ops import ERR_MAGIC, ERR_MAGIC_OVERLAPPING, ERR_NEVER
38
from mypyc.ir.rtypes import (
49
bool_rprimitive,
@@ -387,3 +392,15 @@
387392
error_kind=ERR_NEVER,
388393
dependencies=[LIBRT_STRINGS, STRING_WRITER_EXTRA_OPS],
389394
)
395+
396+
397+
# Codepoint classification helpers operating on i32 codepoints
398+
# (typically obtained via ord(s[i])). Negative inputs return False.
399+
function_op(
400+
name="librt.strings.isspace",
401+
arg_types=[int32_rprimitive],
402+
return_type=bool_rprimitive,
403+
c_function_name="LibRTStrings_IsSpace",
404+
error_kind=ERR_NEVER,
405+
dependencies=[LIBRT_STRINGS, CODEPOINT_EXTRA_OPS],
406+
)

mypyc/test-data/irbuild-librt-strings.test

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,3 +270,78 @@ L1:
270270
L2:
271271
r3 = CPyStringWriter_GetItem(s, r0)
272272
return r3
273+
274+
[case testLibrtStringsIsSpaceIR]
275+
from librt.strings import isspace
276+
from mypy_extensions import i32
277+
278+
def is_ws(c: i32) -> bool:
279+
return isspace(c)
280+
[out]
281+
def is_ws(c):
282+
c :: i32
283+
r0 :: bool
284+
L0:
285+
r0 = LibRTStrings_IsSpace(c)
286+
return r0
287+
288+
[case testLibrtStringsIsSpaceFromStrIndexIR_64bit]
289+
from librt.strings import isspace
290+
291+
def is_ws_at(s: str, i: int) -> bool:
292+
return isspace(ord(s[i]))
293+
[out]
294+
def is_ws_at(s, i):
295+
s :: str
296+
i :: int
297+
r0 :: native_int
298+
r1 :: bit
299+
r2, r3 :: i64
300+
r4 :: ptr
301+
r5 :: c_ptr
302+
r6, r7 :: i64
303+
r8, r9 :: bool
304+
r10 :: short_int
305+
r11, r12 :: bit
306+
r13 :: native_int
307+
r14, r15 :: i32
308+
r16 :: bool
309+
L0:
310+
r0 = i & 1
311+
r1 = r0 == 0
312+
if r1 goto L1 else goto L2 :: bool
313+
L1:
314+
r2 = i >> 1
315+
r3 = r2
316+
goto L3
317+
L2:
318+
r4 = i ^ 1
319+
r5 = r4
320+
r6 = CPyLong_AsInt64(r5)
321+
r3 = r6
322+
keep_alive i
323+
L3:
324+
r7 = CPyStr_AdjustIndex(s, r3)
325+
r8 = CPyStr_RangeCheck(s, r7)
326+
if r8 goto L5 else goto L4 :: bool
327+
L4:
328+
r9 = raise IndexError('index out of range')
329+
unreachable
330+
L5:
331+
r10 = CPyStr_GetItemUnsafeAsInt(s, r7)
332+
r11 = r10 < 4294967296 :: signed
333+
if r11 goto L6 else goto L8 :: bool
334+
L6:
335+
r12 = r10 >= -4294967296 :: signed
336+
if r12 goto L7 else goto L8 :: bool
337+
L7:
338+
r13 = r10 >> 1
339+
r14 = truncate r13: native_int to i32
340+
r15 = r14
341+
goto L9
342+
L8:
343+
CPyInt32_Overflow()
344+
unreachable
345+
L9:
346+
r16 = LibRTStrings_IsSpace(r15)
347+
return r16

mypyc/test-data/run-librt-strings.test

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1439,3 +1439,20 @@ def test_new_without_init_is_usable() -> None:
14391439
assert sw.getvalue() == ""
14401440
sw.write("hello")
14411441
assert sw.getvalue() == "hello"
1442+
1443+
[case testLibrtStringsIsSpace_librt]
1444+
from typing import Any
1445+
from mypy_extensions import i32
1446+
from librt.strings import isspace
1447+
1448+
1449+
def test_isspace() -> None:
1450+
assert not isspace(i32(-1))
1451+
assert not isspace(i32(-113))
1452+
# Verify our codepoint primitive agrees with str.isspace() across all
1453+
# Unicode codepoints, including the ord(chr(i)) round-trip. Any
1454+
# forces generic dispatch on the str side.
1455+
for i in range(0x110000):
1456+
c = chr(i)
1457+
a: Any = c
1458+
assert isspace(ord(c)) == isspace(i) == a.isspace()

0 commit comments

Comments
 (0)