Skip to content

Commit 1bf43aa

Browse files
committed
gh-153569: Add tokenizer source and cursor primitives
1 parent a2d3787 commit 1bf43aa

14 files changed

Lines changed: 904 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
@@ -398,8 +398,10 @@ TOKENIZER_OBJS= \
398398
Parser/lexer/number.o \
399399
Parser/lexer/state.o \
400400
Parser/lexer/string.o \
401+
Parser/tokenizer/cursor.o \
401402
Parser/tokenizer/file_tokenizer.o \
402403
Parser/tokenizer/readline_tokenizer.o \
404+
Parser/tokenizer/source.o \
403405
Parser/tokenizer/string_tokenizer.o \
404406
Parser/tokenizer/utf8_tokenizer.o \
405407
Parser/tokenizer/helpers.o
@@ -414,6 +416,8 @@ TOKENIZER_HEADERS= \
414416
Parser/lexer/lexer.h \
415417
Parser/lexer/lexer_internal.h \
416418
Parser/lexer/state.h \
419+
Parser/tokenizer/cursor.h \
420+
Parser/tokenizer/source.h \
417421
Parser/tokenizer/tokenizer.h \
418422
Parser/tokenizer/helpers.h
419423

@@ -3479,7 +3483,7 @@ MODULE__SOCKET_DEPS=$(srcdir)/Modules/socketmodule.h $(srcdir)/Modules/addrinfo.
34793483
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
34803484
MODULE__TESTCAPI_DEPS=$(srcdir)/Modules/_testcapi/parts.h $(srcdir)/Modules/_testcapi/util.h
34813485
MODULE__TESTLIMITEDCAPI_DEPS=$(srcdir)/Modules/_testlimitedcapi/testcapi_long.h $(srcdir)/Modules/_testlimitedcapi/parts.h $(srcdir)/Modules/_testlimitedcapi/util.h
3482-
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
3486+
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
34833487
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
34843488
MODULE__ZSTD_DEPS=$(srcdir)/Modules/_zstd/_zstdmodule.h $(srcdir)/Modules/_zstd/buffer.h $(srcdir)/Modules/_zstd/zstddict.h
34853489

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
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
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/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
178178
@MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c

Modules/_testinternalcapi.c

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

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

2021
#endif // Py_TESTINTERNALCAPI_PARTS_H
Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
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_SourceLine(&source, 3, &line) == 0,
107+
"cannot find third source line") < 0 ||
108+
check(line.contains_nul, "missing null byte flag") < 0 ||
109+
check(_PyTok_SourceLine(&source, final_line, &line) == 0,
110+
"cannot find final source line") < 0 ||
111+
check(line.implicit_newline,
112+
"missing late implicit newline flag") < 0) {
113+
goto error;
114+
}
115+
116+
Py_ssize_t view_len;
117+
const char *view = _PyTok_SourceSpanView(
118+
&source, _PyTok_SpanFromBounds(6, 8), &view_len);
119+
if (check(view != NULL && view_len == 2 &&
120+
memcmp(view, "\xce\xb2", 2) == 0,
121+
"wrong source span view") < 0 ||
122+
check(_PyTok_SourceLocation(
123+
&source, marker_start,
124+
_PYTOK_AFFINITY_LEFT, &loc) == 0,
125+
"cannot locate left line boundary") < 0 ||
126+
check(loc.lineno == marker_line - 1 && loc.byte_col == 2,
127+
"wrong left boundary location") < 0 ||
128+
check(_PyTok_SourceLocation(
129+
&source, marker_start,
130+
_PYTOK_AFFINITY_RIGHT, &loc) == 0,
131+
"cannot locate right line boundary") < 0 ||
132+
check(loc.lineno == marker_line && loc.byte_col == 0,
133+
"wrong right boundary location") < 0 ||
134+
check(_PyTok_SourceLocation(
135+
&source, marker_start + 1,
136+
_PYTOK_AFFINITY_RIGHT, &loc) == 0,
137+
"cannot locate late source byte") < 0 ||
138+
check(loc.lineno == marker_line && loc.byte_col == 1,
139+
"wrong late source location") < 0) {
140+
goto error;
141+
}
142+
143+
if (check(_PyTok_SourceLocation(
144+
&source, source.len, _PYTOK_AFFINITY_LEFT, &loc) == 0,
145+
"cannot locate left EOF") < 0 ||
146+
check(loc.lineno == final_line && loc.byte_col == 2,
147+
"wrong left EOF location") < 0 ||
148+
check(_PyTok_SourceLocation(
149+
&source, source.len,
150+
_PYTOK_AFFINITY_RIGHT, &loc) == 0,
151+
"cannot locate right EOF") < 0 ||
152+
check(loc.lineno == final_line + 1 && loc.byte_col == 0,
153+
"wrong right EOF location") < 0 ||
154+
check(_PyTok_SourceLine(&source, final_line + 1, &line) == 0,
155+
"cannot find virtual EOF line") < 0 ||
156+
check(line.start == source.len && line.end == source.len,
157+
"wrong virtual EOF line") < 0) {
158+
goto error;
159+
}
160+
161+
view = _PyTok_SourceSpanView(
162+
&source, _PyTok_SpanFromBounds(0, source.len + 1), &view_len);
163+
if (check_system_error(view == NULL, "accepted invalid source span") < 0 ||
164+
check_system_error(
165+
_PyTok_SourceLocation(
166+
&source, source.len + 1,
167+
_PYTOK_AFFINITY_RIGHT, &loc) < 0,
168+
"accepted invalid source offset") < 0 ||
169+
check_system_error(
170+
_PyTok_SourceLine(&source, final_line + 2, &line) < 0,
171+
"accepted invalid source line") < 0) {
172+
goto error;
173+
}
174+
175+
_PyTok_SourceClear(&source);
176+
_PyTok_SourceInit(&source);
177+
if (_PyTok_SourceAppendLine(&source, "tail", 4, 0) < 0 ||
178+
check_system_error(
179+
_PyTok_SourceAppendLine(&source, "x\n", 2, 0) < 0,
180+
"appended after unterminated source line") < 0 ||
181+
check(_PyTok_SourceLocation(
182+
&source, source.len,
183+
_PYTOK_AFFINITY_RIGHT, &loc) == 0,
184+
"cannot locate unterminated EOF") < 0 ||
185+
check(loc.lineno == 1 && loc.byte_col == 4,
186+
"wrong unterminated EOF location") < 0) {
187+
goto error;
188+
}
189+
190+
_PyTok_SourceClear(&source);
191+
Py_RETURN_NONE;
192+
193+
error:
194+
_PyTok_SourceClear(&source);
195+
return NULL;
196+
}
197+
198+
static PyObject *
199+
test_tokenizer_cursor(PyObject *Py_UNUSED(module),
200+
PyObject *Py_UNUSED(args))
201+
{
202+
_PyTok_SourceText source;
203+
_PyTok_SourceInit(&source);
204+
if (_PyTok_SourceAppendLine(&source, "ab\n", 3, 0) < 0 ||
205+
_PyTok_SourceAppendLine(&source, "cd\n", 3, 0) < 0) {
206+
goto error;
207+
}
208+
209+
_PyTok_Cursor cursor;
210+
_PyTok_CursorInit(&cursor, &source);
211+
if (_PyTok_CursorSetOffset(&cursor, source.len) < 0 ||
212+
check(cursor.lineno == 3 && cursor.pos == source.len,
213+
"wrong cursor at virtual EOF") < 0 ||
214+
_PyTok_CursorSetLine(&cursor, 1) < 0) {
215+
goto error;
216+
}
217+
218+
char large[BUFSIZ + 1];
219+
memset(large, 'z', sizeof(large));
220+
large[sizeof(large) - 1] = '\n';
221+
if (_PyTok_SourceAppendLine(&source, large, sizeof(large), 0) < 0) {
222+
goto error;
223+
}
224+
225+
if (check(_PyTok_CursorPeek(&cursor, 0) == 'a',
226+
"wrong cursor peek after relocation") < 0 ||
227+
check(_PyTok_CursorPeek(&cursor, 1) == 'b',
228+
"wrong distant cursor peek") < 0 ||
229+
check(_PyTok_CursorAdvance(&cursor) == 'a',
230+
"wrong first cursor byte") < 0 ||
231+
check(_PyTok_CursorAdvance(&cursor) == 'b',
232+
"wrong second cursor byte") < 0 ||
233+
check(_PyTok_CursorAdvance(&cursor) == '\n',
234+
"wrong final cursor byte") < 0 ||
235+
check(_PyTok_CursorAdvance(&cursor) == EOF,
236+
"cursor advanced past line") < 0 ||
237+
check(_PyTok_CursorSetOffset(&cursor, 2) == 0,
238+
"cannot seek cursor offset") < 0 ||
239+
check(_PyTok_CursorAdvance(&cursor) == '\n',
240+
"wrong cursor byte after seek") < 0 ||
241+
check(_PyTok_CursorSetOffset(&cursor, 3) == 0,
242+
"cannot seek line boundary") < 0 ||
243+
check(cursor.lineno == 2 && cursor.line_start == 3 &&
244+
_PyTok_CursorAdvance(&cursor) == 'c',
245+
"wrong cursor at line boundary") < 0 ||
246+
check(_PyTok_CursorSetLine(&cursor, 3) == 0,
247+
"cannot advance cursor to final line") < 0 ||
248+
check(cursor.line_start == 6 &&
249+
_PyTok_CursorAdvance(&cursor) == 'z',
250+
"wrong cursor byte on final line") < 0) {
251+
goto error;
252+
}
253+
254+
_PyTok_Cursor saved = cursor;
255+
if (check_system_error(
256+
_PyTok_CursorSetOffset(&cursor, source.len + 1) < 0,
257+
"accepted invalid cursor offset") < 0 ||
258+
check(same_cursor(&cursor, &saved),
259+
"invalid offset changed cursor") < 0 ||
260+
check_system_error(
261+
_PyTok_CursorSetLine(&cursor, source.nlines + 2) < 0,
262+
"accepted invalid cursor line") < 0 ||
263+
check(same_cursor(&cursor, &saved),
264+
"invalid line changed cursor") < 0 ||
265+
check(_PyTok_CursorSetOffset(&cursor, source.len) == 0,
266+
"cannot set cursor to EOF") < 0 ||
267+
check(cursor.lineno == 4 && cursor.pos == source.len,
268+
"wrong cursor at EOF") < 0) {
269+
goto error;
270+
}
271+
272+
#if SIZEOF_VOID_P > 4
273+
char byte = 0;
274+
_PyTok_SourceText huge_source = {
275+
.bytes = &byte,
276+
.len = (_PyTok_Off)INT_MAX + 1,
277+
};
278+
_PyTok_Cursor huge_cursor = {
279+
.source = &huge_source,
280+
.pos = INT_MAX,
281+
.line_end = (_PyTok_Off)INT_MAX + 1,
282+
.lineno = 1,
283+
};
284+
if (check(_PyTok_CursorAdvance(&huge_cursor) == EOF &&
285+
huge_cursor.pos == INT_MAX,
286+
"cursor advanced past maximum column") < 0) {
287+
goto error;
288+
}
289+
#endif
290+
291+
_PyTok_SourceClear(&source);
292+
Py_RETURN_NONE;
293+
294+
error:
295+
_PyTok_SourceClear(&source);
296+
return NULL;
297+
}
298+
299+
static PyMethodDef test_methods[] = {
300+
{"test_tokenizer_source", test_tokenizer_source, METH_NOARGS},
301+
{"test_tokenizer_cursor", test_tokenizer_cursor, METH_NOARGS},
302+
{NULL},
303+
};
304+
305+
int
306+
_PyTestInternalCapi_Init_Tokenizer(PyObject *module)
307+
{
308+
return PyModule_AddFunctions(module, test_methods);
309+
}

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
</ItemGroup>
105106
<ItemGroup>

0 commit comments

Comments
 (0)