Skip to content

Commit f54fd2a

Browse files
authored
gh-153569: Add tokenizer source and cursor primitives (#153587)
1 parent 7f0ccd6 commit f54fd2a

14 files changed

Lines changed: 923 additions & 2 deletions
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import unittest
2+
from test.support import import_helper
3+
4+
5+
_testinternalcapi = import_helper.import_module("_testinternalcapi")
6+
7+
8+
class TokenizerTests(unittest.TestCase):
9+
def test_source(self):
10+
_testinternalcapi.test_tokenizer_source()
11+
12+
def test_cursor(self):
13+
_testinternalcapi.test_tokenizer_cursor()
14+
15+
16+
if __name__ == "__main__":
17+
unittest.main()

Makefile.pre.in

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -399,8 +399,10 @@ TOKENIZER_OBJS= \
399399
Parser/lexer/number.o \
400400
Parser/lexer/state.o \
401401
Parser/lexer/string.o \
402+
Parser/tokenizer/cursor.o \
402403
Parser/tokenizer/file_tokenizer.o \
403404
Parser/tokenizer/readline_tokenizer.o \
405+
Parser/tokenizer/source.o \
404406
Parser/tokenizer/string_tokenizer.o \
405407
Parser/tokenizer/utf8_tokenizer.o \
406408
Parser/tokenizer/helpers.o
@@ -415,6 +417,8 @@ TOKENIZER_HEADERS= \
415417
Parser/lexer/lexer.h \
416418
Parser/lexer/lexer_internal.h \
417419
Parser/lexer/state.h \
420+
Parser/tokenizer/cursor.h \
421+
Parser/tokenizer/source.h \
418422
Parser/tokenizer/tokenizer.h \
419423
Parser/tokenizer/helpers.h
420424

@@ -3458,7 +3462,7 @@ MODULE__SOCKET_DEPS=$(srcdir)/Modules/socketmodule.h $(srcdir)/Modules/addrinfo.
34583462
MODULE__SSL_DEPS=$(srcdir)/Modules/_ssl.h $(srcdir)/Modules/_ssl/cert.c $(srcdir)/Modules/_ssl/debughelpers.c $(srcdir)/Modules/_ssl/misc.c $(srcdir)/Modules/_ssl_data_111.h $(srcdir)/Modules/_ssl_data_300.h $(srcdir)/Modules/socketmodule.h
34593463
MODULE__TESTCAPI_DEPS=$(srcdir)/Modules/_testcapi/parts.h $(srcdir)/Modules/_testcapi/util.h
34603464
MODULE__TESTLIMITEDCAPI_DEPS=$(srcdir)/Modules/_testlimitedcapi/testcapi_long.h $(srcdir)/Modules/_testlimitedcapi/parts.h $(srcdir)/Modules/_testlimitedcapi/util.h
3461-
MODULE__TESTINTERNALCAPI_DEPS=$(srcdir)/Modules/_testinternalcapi/parts.h $(srcdir)/Python/ceval.h $(srcdir)/Modules/_testinternalcapi/test_targets.h $(srcdir)/Modules/_testinternalcapi/test_cases.c.h
3465+
MODULE__TESTINTERNALCAPI_DEPS=$(srcdir)/Modules/_testinternalcapi/parts.h $(srcdir)/Parser/tokenizer/cursor.h $(srcdir)/Parser/tokenizer/source.h $(srcdir)/Python/ceval.h $(srcdir)/Modules/_testinternalcapi/test_targets.h $(srcdir)/Modules/_testinternalcapi/test_cases.c.h
34623466
MODULE__SQLITE3_DEPS=$(srcdir)/Modules/_sqlite/connection.h $(srcdir)/Modules/_sqlite/cursor.h $(srcdir)/Modules/_sqlite/microprotocols.h $(srcdir)/Modules/_sqlite/module.h $(srcdir)/Modules/_sqlite/prepare_protocol.h $(srcdir)/Modules/_sqlite/row.h $(srcdir)/Modules/_sqlite/util.h
34633467
MODULE__ZSTD_DEPS=$(srcdir)/Modules/_zstd/_zstdmodule.h $(srcdir)/Modules/_zstd/buffer.h $(srcdir)/Modules/_zstd/zstddict.h
34643468

Modules/Setup.stdlib.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@
172172
@MODULE_XXSUBTYPE_TRUE@xxsubtype xxsubtype.c
173173
@MODULE__XXTESTFUZZ_TRUE@_xxtestfuzz _xxtestfuzz/_xxtestfuzz.c _xxtestfuzz/fuzzer.c
174174
@MODULE__TESTBUFFER_TRUE@_testbuffer _testbuffer.c
175-
@MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c _testinternalcapi/test_lock.c _testinternalcapi/pytime.c _testinternalcapi/set.c _testinternalcapi/test_critical_sections.c _testinternalcapi/complex.c _testinternalcapi/interpreter.c _testinternalcapi/tuple.c _testinternalcapi/typecache.c
175+
@MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c _testinternalcapi/test_lock.c _testinternalcapi/pytime.c _testinternalcapi/set.c _testinternalcapi/test_critical_sections.c _testinternalcapi/complex.c _testinternalcapi/interpreter.c _testinternalcapi/tokenizer.c _testinternalcapi/tuple.c _testinternalcapi/typecache.c
176176
@MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/unicode.c _testcapi/dict.c _testcapi/set.c _testcapi/list.c _testcapi/tuple.c _testcapi/getargs.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/complex.c _testcapi/numbers.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/buffer.c _testcapi/pyatomic.c _testcapi/run.c _testcapi/file.c _testcapi/codec.c _testcapi/immortal.c _testcapi/gc.c _testcapi/hash.c _testcapi/time.c _testcapi/bytes.c _testcapi/object.c _testcapi/modsupport.c _testcapi/monitoring.c _testcapi/config.c _testcapi/import.c _testcapi/frame.c _testcapi/type.c _testcapi/function.c _testcapi/module.c _testcapi/weakref.c
177177
@MODULE__TESTLIMITEDCAPI_TRUE@_testlimitedcapi _testlimitedcapi.c _testlimitedcapi/abstract.c _testlimitedcapi/bytearray.c _testlimitedcapi/bytes.c _testlimitedcapi/capsule.c _testlimitedcapi/codec.c _testlimitedcapi/complex.c _testlimitedcapi/dict.c _testlimitedcapi/eval.c _testlimitedcapi/float.c _testlimitedcapi/heaptype_relative.c _testlimitedcapi/import.c _testlimitedcapi/list.c _testlimitedcapi/long.c _testlimitedcapi/object.c _testlimitedcapi/pyos.c _testlimitedcapi/set.c _testlimitedcapi/slots.c _testlimitedcapi/sys.c _testlimitedcapi/threadstate.c _testlimitedcapi/tuple.c _testlimitedcapi/unicode.c _testlimitedcapi/vectorcall_limited.c _testlimitedcapi/version.c _testlimitedcapi/file.c _testlimitedcapi/weakref.c _testlimitedcapi/run.c _testlimitedcapi/type.c
178178
@MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c

Modules/_testinternalcapi.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3419,6 +3419,9 @@ module_exec(PyObject *module)
34193419
if (_PyTestInternalCapi_Init_CriticalSection(module) < 0) {
34203420
return 1;
34213421
}
3422+
if (_PyTestInternalCapi_Init_Tokenizer(module) < 0) {
3423+
return 1;
3424+
}
34223425
if (_PyTestInternalCapi_Init_Tuple(module) < 0) {
34233426
return 1;
34243427
}

Modules/_testinternalcapi/parts.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ int _PyTestInternalCapi_Init_PyTime(PyObject *module);
1515
int _PyTestInternalCapi_Init_Set(PyObject *module);
1616
int _PyTestInternalCapi_Init_Complex(PyObject *module);
1717
int _PyTestInternalCapi_Init_CriticalSection(PyObject *module);
18+
int _PyTestInternalCapi_Init_Tokenizer(PyObject *module);
1819
int _PyTestInternalCapi_Init_Tuple(PyObject *module);
1920
int _PyTestInternalCapi_Init_TypeCache(PyObject *module);
2021

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
#include "parts.h"
2+
3+
#include "../../Parser/tokenizer/cursor.h"
4+
5+
static int
6+
check(int condition, const char *message)
7+
{
8+
if (condition) {
9+
return 0;
10+
}
11+
PyErr_SetString(PyExc_AssertionError, message);
12+
return -1;
13+
}
14+
15+
static int
16+
check_system_error(int failed, const char *message)
17+
{
18+
if (!failed || !PyErr_ExceptionMatches(PyExc_SystemError)) {
19+
PyErr_SetString(PyExc_AssertionError, message);
20+
return -1;
21+
}
22+
PyErr_Clear();
23+
return 0;
24+
}
25+
26+
static int
27+
same_cursor(const _PyTok_Cursor *left, const _PyTok_Cursor *right)
28+
{
29+
return left->source == right->source &&
30+
left->pos == right->pos &&
31+
left->line_start == right->line_start &&
32+
left->line_end == right->line_end &&
33+
left->lineno == right->lineno;
34+
}
35+
36+
static PyObject *
37+
test_tokenizer_source(PyObject *Py_UNUSED(module),
38+
PyObject *Py_UNUSED(args))
39+
{
40+
_PyTok_SourceText source;
41+
_PyTok_SourceInit(&source);
42+
43+
_PyTok_Loc loc;
44+
_PyTok_Line line;
45+
if (check(_PyTok_SourceLocation(
46+
&source, 0, _PYTOK_AFFINITY_RIGHT, &loc) == 0,
47+
"cannot locate empty source") < 0 ||
48+
check(loc.lineno == 1 && loc.byte_col == 0,
49+
"wrong empty source location") < 0 ||
50+
check(_PyTok_SourceLine(&source, 1, &line) == 0,
51+
"cannot find empty source line") < 0 ||
52+
check(line.start == 0 && line.end == 0,
53+
"wrong empty source line") < 0 ||
54+
check_system_error(
55+
_PyTok_SourceAppendLine(&source, "", 0, 0) < 0,
56+
"accepted empty source line") < 0 ||
57+
check_system_error(
58+
_PyTok_SourceAppendLine(&source, "a\nb\n", 4, 0) < 0,
59+
"accepted multiple source lines") < 0 ||
60+
check_system_error(
61+
_PyTok_SourceAppendLine(&source, "a", 1, 1) < 0,
62+
"accepted missing implicit newline") < 0) {
63+
goto error;
64+
}
65+
66+
if (check(_PyTok_SourceAppendLine(&source, "alpha\n", 6, 0) == 0,
67+
"wrong first source offset") < 0 ||
68+
check(_PyTok_SourceAppendLine(
69+
&source, "\xce\xb2\n", 3, 1) == 6,
70+
"wrong second source offset") < 0 ||
71+
check(_PyTok_SourceAppendLine(
72+
&source, "nul\0x\n", 6, 0) == 9,
73+
"wrong third source offset") < 0) {
74+
goto error;
75+
}
76+
77+
int marker_line = 257;
78+
int final_line = 300;
79+
_PyTok_Off marker_start = -1;
80+
for (int lineno = 4; lineno <= final_line; lineno++) {
81+
const char *text = lineno == marker_line ? "marker\n" : "x\n";
82+
Py_ssize_t len = (Py_ssize_t)strlen(text);
83+
_PyTok_Off start = _PyTok_SourceAppendLine(
84+
&source, text, len, lineno == final_line);
85+
if (start < 0) {
86+
goto error;
87+
}
88+
if (lineno == marker_line) {
89+
marker_start = start;
90+
}
91+
}
92+
93+
if (check(source.nlines == final_line, "wrong source line count") < 0 ||
94+
check(_PyTok_SourceLine(&source, marker_line, &line) == 0,
95+
"cannot find late source line") < 0 ||
96+
check(line.start == marker_start &&
97+
line.end == marker_start + 7,
98+
"wrong late source line") < 0 ||
99+
check(!line.implicit_newline && !line.contains_nul,
100+
"wrong late source flags") < 0 ||
101+
check(_PyTok_SourceLine(&source, 2, &line) == 0,
102+
"cannot find second source line") < 0 ||
103+
check(line.start == 6 && line.end == 9 &&
104+
line.implicit_newline && !line.contains_nul,
105+
"wrong second source line") < 0 ||
106+
check(!_PyTok_SourceLineIsImplicit(&source, 1) &&
107+
_PyTok_SourceLineIsImplicit(&source, 2),
108+
"wrong early implicit newline flags") < 0 ||
109+
check(_PyTok_SourceLine(&source, 3, &line) == 0,
110+
"cannot find third source line") < 0 ||
111+
check(line.contains_nul, "missing null byte flag") < 0 ||
112+
check(_PyTok_SourceLine(&source, final_line, &line) == 0,
113+
"cannot find final source line") < 0 ||
114+
check(line.implicit_newline &&
115+
_PyTok_SourceLineIsImplicit(&source, final_line),
116+
"missing late implicit newline flag") < 0) {
117+
goto error;
118+
}
119+
120+
Py_ssize_t view_len;
121+
const char *view = _PyTok_SourceSpanView(
122+
&source, _PyTok_SpanFromBounds(6, 8), &view_len);
123+
if (check(view != NULL && view_len == 2 &&
124+
memcmp(view, "\xce\xb2", 2) == 0,
125+
"wrong source span view") < 0 ||
126+
check(_PyTok_SourceLocation(
127+
&source, marker_start,
128+
_PYTOK_AFFINITY_LEFT, &loc) == 0,
129+
"cannot locate left line boundary") < 0 ||
130+
check(loc.lineno == marker_line - 1 && loc.byte_col == 2,
131+
"wrong left boundary location") < 0 ||
132+
check(_PyTok_SourceLocation(
133+
&source, marker_start,
134+
_PYTOK_AFFINITY_RIGHT, &loc) == 0,
135+
"cannot locate right line boundary") < 0 ||
136+
check(loc.lineno == marker_line && loc.byte_col == 0,
137+
"wrong right boundary location") < 0 ||
138+
check(_PyTok_SourceLocation(
139+
&source, marker_start + 1,
140+
_PYTOK_AFFINITY_RIGHT, &loc) == 0,
141+
"cannot locate late source byte") < 0 ||
142+
check(loc.lineno == marker_line && loc.byte_col == 1,
143+
"wrong late source location") < 0) {
144+
goto error;
145+
}
146+
147+
if (check(_PyTok_SourceLocation(
148+
&source, source.len, _PYTOK_AFFINITY_LEFT, &loc) == 0,
149+
"cannot locate left EOF") < 0 ||
150+
check(loc.lineno == final_line && loc.byte_col == 2,
151+
"wrong left EOF location") < 0 ||
152+
check(_PyTok_SourceLocation(
153+
&source, source.len,
154+
_PYTOK_AFFINITY_RIGHT, &loc) == 0,
155+
"cannot locate right EOF") < 0 ||
156+
check(loc.lineno == final_line + 1 && loc.byte_col == 0,
157+
"wrong right EOF location") < 0 ||
158+
check(_PyTok_SourceLine(&source, final_line + 1, &line) == 0,
159+
"cannot find virtual EOF line") < 0 ||
160+
check(line.start == source.len && line.end == source.len,
161+
"wrong virtual EOF line") < 0 ||
162+
check(!_PyTok_SourceLineIsImplicit(&source, 0) &&
163+
!_PyTok_SourceLineIsImplicit(
164+
&source, final_line + 1),
165+
"virtual or invalid line is implicit") < 0) {
166+
goto error;
167+
}
168+
169+
view = _PyTok_SourceSpanView(
170+
&source, _PyTok_SpanFromBounds(0, source.len + 1), &view_len);
171+
if (check_system_error(view == NULL, "accepted invalid source span") < 0 ||
172+
check_system_error(
173+
_PyTok_SourceLocation(
174+
&source, source.len + 1,
175+
_PYTOK_AFFINITY_RIGHT, &loc) < 0,
176+
"accepted invalid source offset") < 0 ||
177+
check_system_error(
178+
_PyTok_SourceLine(&source, final_line + 2, &line) < 0,
179+
"accepted invalid source line") < 0) {
180+
goto error;
181+
}
182+
183+
_PyTok_SourceClear(&source);
184+
_PyTok_SourceInit(&source);
185+
if (_PyTok_SourceAppendLine(&source, "tail", 4, 0) < 0 ||
186+
check_system_error(
187+
_PyTok_SourceAppendLine(&source, "x\n", 2, 0) < 0,
188+
"appended after unterminated source line") < 0 ||
189+
check(_PyTok_SourceLocation(
190+
&source, source.len,
191+
_PYTOK_AFFINITY_RIGHT, &loc) == 0,
192+
"cannot locate unterminated EOF") < 0 ||
193+
check(loc.lineno == 1 && loc.byte_col == 4,
194+
"wrong unterminated EOF location") < 0) {
195+
goto error;
196+
}
197+
198+
_PyTok_SourceClear(&source);
199+
Py_RETURN_NONE;
200+
201+
error:
202+
_PyTok_SourceClear(&source);
203+
return NULL;
204+
}
205+
206+
static PyObject *
207+
test_tokenizer_cursor(PyObject *Py_UNUSED(module),
208+
PyObject *Py_UNUSED(args))
209+
{
210+
_PyTok_SourceText source;
211+
_PyTok_SourceInit(&source);
212+
if (_PyTok_SourceAppendLine(&source, "ab\n", 3, 0) < 0 ||
213+
_PyTok_SourceAppendLine(&source, "cd\n", 3, 0) < 0) {
214+
goto error;
215+
}
216+
217+
_PyTok_Cursor cursor;
218+
_PyTok_CursorInit(&cursor, &source);
219+
if (_PyTok_CursorSetOffset(&cursor, source.len) < 0 ||
220+
check(cursor.lineno == 3 && cursor.pos == source.len,
221+
"wrong cursor at virtual EOF") < 0 ||
222+
_PyTok_CursorSetLine(&cursor, 1) < 0) {
223+
goto error;
224+
}
225+
226+
char large[BUFSIZ + 1];
227+
memset(large, 'z', sizeof(large));
228+
large[sizeof(large) - 1] = '\n';
229+
if (_PyTok_SourceAppendLine(&source, large, sizeof(large), 0) < 0) {
230+
goto error;
231+
}
232+
233+
if (check(_PyTok_CursorPeek(&cursor, 0) == 'a',
234+
"wrong cursor peek after relocation") < 0 ||
235+
check(_PyTok_CursorPeek(&cursor, 1) == 'b',
236+
"wrong distant cursor peek") < 0 ||
237+
check(_PyTok_CursorAdvance(&cursor) == 'a',
238+
"wrong first cursor byte") < 0 ||
239+
check(_PyTok_CursorAdvance(&cursor) == 'b',
240+
"wrong second cursor byte") < 0 ||
241+
check(_PyTok_CursorAdvance(&cursor) == '\n',
242+
"wrong final cursor byte") < 0 ||
243+
check(_PyTok_CursorAdvance(&cursor) == EOF,
244+
"cursor advanced past line") < 0 ||
245+
check(_PyTok_CursorSetOffset(&cursor, 2) == 0,
246+
"cannot seek cursor offset") < 0 ||
247+
check(_PyTok_CursorAdvance(&cursor) == '\n',
248+
"wrong cursor byte after seek") < 0 ||
249+
check(_PyTok_CursorSetOffset(&cursor, 3) == 0,
250+
"cannot seek line boundary") < 0 ||
251+
check(cursor.lineno == 2 && cursor.line_start == 3 &&
252+
_PyTok_CursorAdvance(&cursor) == 'c',
253+
"wrong cursor at line boundary") < 0 ||
254+
check(_PyTok_CursorSetLine(&cursor, 3) == 0,
255+
"cannot advance cursor to final line") < 0 ||
256+
check(cursor.line_start == 6 &&
257+
_PyTok_CursorAdvance(&cursor) == 'z',
258+
"wrong cursor byte on final line") < 0) {
259+
goto error;
260+
}
261+
262+
_PyTok_Cursor saved = cursor;
263+
if (check_system_error(
264+
_PyTok_CursorSetOffset(&cursor, source.len + 1) < 0,
265+
"accepted invalid cursor offset") < 0 ||
266+
check(same_cursor(&cursor, &saved),
267+
"invalid offset changed cursor") < 0 ||
268+
check_system_error(
269+
_PyTok_CursorSetLine(&cursor, source.nlines + 2) < 0,
270+
"accepted invalid cursor line") < 0 ||
271+
check(same_cursor(&cursor, &saved),
272+
"invalid line changed cursor") < 0 ||
273+
check(_PyTok_CursorSetOffset(&cursor, source.len) == 0,
274+
"cannot set cursor to EOF") < 0 ||
275+
check(cursor.lineno == 4 && cursor.pos == source.len,
276+
"wrong cursor at EOF") < 0) {
277+
goto error;
278+
}
279+
280+
#if SIZEOF_VOID_P > 4
281+
char byte = 0;
282+
_PyTok_SourceText huge_source = {
283+
.bytes = &byte,
284+
.len = (_PyTok_Off)INT_MAX + 1,
285+
};
286+
_PyTok_Cursor huge_cursor = {
287+
.source = &huge_source,
288+
.pos = INT_MAX,
289+
.line_end = (_PyTok_Off)INT_MAX + 1,
290+
.lineno = 1,
291+
};
292+
if (check(_PyTok_CursorAdvance(&huge_cursor) == EOF &&
293+
huge_cursor.pos == INT_MAX,
294+
"cursor advanced past maximum column") < 0) {
295+
goto error;
296+
}
297+
#endif
298+
299+
_PyTok_SourceClear(&source);
300+
Py_RETURN_NONE;
301+
302+
error:
303+
_PyTok_SourceClear(&source);
304+
return NULL;
305+
}
306+
307+
static PyMethodDef test_methods[] = {
308+
{"test_tokenizer_source", test_tokenizer_source, METH_NOARGS},
309+
{"test_tokenizer_cursor", test_tokenizer_cursor, METH_NOARGS},
310+
{NULL},
311+
};
312+
313+
int
314+
_PyTestInternalCapi_Init_Tokenizer(PyObject *module)
315+
{
316+
return PyModule_AddFunctions(module, test_methods);
317+
}

PCbuild/_testinternalcapi.vcxproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@
100100
<ClCompile Include="..\Modules\_testinternalcapi\set.c" />
101101
<ClCompile Include="..\Modules\_testinternalcapi\complex.c" />
102102
<ClCompile Include="..\Modules\_testinternalcapi\interpreter.c" />
103+
<ClCompile Include="..\Modules\_testinternalcapi\tokenizer.c" />
103104
<ClCompile Include="..\Modules\_testinternalcapi\tuple.c" />
104105
<ClCompile Include="..\Modules\_testinternalcapi\typecache.c" />
105106
</ItemGroup>

0 commit comments

Comments
 (0)