dissect.cstruct 4.8.dev9__py3-none-any.whl → 5.0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dissect/cstruct/cstruct.py +4 -10
- dissect/cstruct/exceptions.py +4 -4
- dissect/cstruct/expression.py +189 -271
- dissect/cstruct/lexer.py +574 -0
- dissect/cstruct/parser.py +425 -592
- dissect/cstruct/utils.py +24 -10
- {dissect_cstruct-4.8.dev9.dist-info → dissect_cstruct-5.0.dev0.dist-info}/METADATA +1 -1
- {dissect_cstruct-4.8.dev9.dist-info → dissect_cstruct-5.0.dev0.dist-info}/RECORD +15 -14
- {dissect_cstruct-4.8.dev9.dist-info → dissect_cstruct-5.0.dev0.dist-info}/scm_file_list.json +52 -50
- dissect_cstruct-5.0.dev0.dist-info/scm_version.json +8 -0
- dissect_cstruct-4.8.dev9.dist-info/scm_version.json +0 -8
- {dissect_cstruct-4.8.dev9.dist-info → dissect_cstruct-5.0.dev0.dist-info}/WHEEL +0 -0
- {dissect_cstruct-4.8.dev9.dist-info → dissect_cstruct-5.0.dev0.dist-info}/entry_points.txt +0 -0
- {dissect_cstruct-4.8.dev9.dist-info → dissect_cstruct-5.0.dev0.dist-info}/licenses/COPYRIGHT +0 -0
- {dissect_cstruct-4.8.dev9.dist-info → dissect_cstruct-5.0.dev0.dist-info}/licenses/LICENSE +0 -0
- {dissect_cstruct-4.8.dev9.dist-info → dissect_cstruct-5.0.dev0.dist-info}/top_level.txt +0 -0
dissect/cstruct/lexer.py
ADDED
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import enum
|
|
4
|
+
import re
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from dissect.cstruct.exceptions import LexerError
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TokenType(enum.Enum):
|
|
14
|
+
# Identifiers & literals
|
|
15
|
+
IDENTIFIER = "IDENTIFIER"
|
|
16
|
+
NUMBER = "NUMBER"
|
|
17
|
+
STRING = "STRING"
|
|
18
|
+
BYTES = "BYTES"
|
|
19
|
+
|
|
20
|
+
# Punctuation
|
|
21
|
+
LBRACE = "{"
|
|
22
|
+
RBRACE = "}"
|
|
23
|
+
LBRACKET = "["
|
|
24
|
+
RBRACKET = "]"
|
|
25
|
+
LPAREN = "("
|
|
26
|
+
RPAREN = ")"
|
|
27
|
+
SEMICOLON = ";"
|
|
28
|
+
COMMA = ","
|
|
29
|
+
COLON = ":"
|
|
30
|
+
QUESTION = "?"
|
|
31
|
+
STAR = "*"
|
|
32
|
+
EQUALS = "="
|
|
33
|
+
|
|
34
|
+
# Keywords
|
|
35
|
+
STRUCT = "STRUCT"
|
|
36
|
+
UNION = "UNION"
|
|
37
|
+
ENUM = "ENUM"
|
|
38
|
+
FLAG = "FLAG"
|
|
39
|
+
TYPEDEF = "TYPEDEF"
|
|
40
|
+
SIZEOF = "SIZEOF"
|
|
41
|
+
OFFSETOF = "OFFSETOF"
|
|
42
|
+
|
|
43
|
+
# Operators
|
|
44
|
+
PLUS = "+"
|
|
45
|
+
MINUS = "-"
|
|
46
|
+
UNARY_MINUS = "-u"
|
|
47
|
+
SLASH = "/"
|
|
48
|
+
PERCENT = "%"
|
|
49
|
+
AMPERSAND = "&"
|
|
50
|
+
PIPE = "|"
|
|
51
|
+
CARET = "^"
|
|
52
|
+
TILDE = "~"
|
|
53
|
+
LSHIFT = "<<"
|
|
54
|
+
RSHIFT = ">>"
|
|
55
|
+
|
|
56
|
+
# Preprocessor
|
|
57
|
+
PP_DEFINE = "PP_DEFINE"
|
|
58
|
+
PP_UNDEF = "PP_UNDEF"
|
|
59
|
+
PP_IFDEF = "PP_IFDEF"
|
|
60
|
+
PP_IFNDEF = "PP_IFNDEF"
|
|
61
|
+
PP_ELSE = "PP_ELSE"
|
|
62
|
+
PP_ENDIF = "PP_ENDIF"
|
|
63
|
+
PP_INCLUDE = "PP_INCLUDE"
|
|
64
|
+
PP_FLAGS = "PP_FLAGS"
|
|
65
|
+
|
|
66
|
+
# Special
|
|
67
|
+
EOF = "EOF"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class Token:
|
|
71
|
+
__slots__ = ("column", "line", "type", "value")
|
|
72
|
+
|
|
73
|
+
def __init__(self, type: TokenType, value: str, line: int, column: int = 0):
|
|
74
|
+
self.type = type
|
|
75
|
+
self.value = value
|
|
76
|
+
self.line = line
|
|
77
|
+
self.column = column
|
|
78
|
+
|
|
79
|
+
def __repr__(self) -> str:
|
|
80
|
+
return f"<Token.{self.type.name} value={self.value!r} line={self.line}>"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
_PP_KEYWORDS = {
|
|
84
|
+
"define": TokenType.PP_DEFINE,
|
|
85
|
+
"undef": TokenType.PP_UNDEF,
|
|
86
|
+
"ifdef": TokenType.PP_IFDEF,
|
|
87
|
+
"ifndef": TokenType.PP_IFNDEF,
|
|
88
|
+
"else": TokenType.PP_ELSE,
|
|
89
|
+
"endif": TokenType.PP_ENDIF,
|
|
90
|
+
"include": TokenType.PP_INCLUDE,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
_C_KEYWORDS = {
|
|
94
|
+
"sizeof": TokenType.SIZEOF,
|
|
95
|
+
"offsetof": TokenType.OFFSETOF,
|
|
96
|
+
"struct": TokenType.STRUCT,
|
|
97
|
+
"union": TokenType.UNION,
|
|
98
|
+
"enum": TokenType.ENUM,
|
|
99
|
+
"flag": TokenType.FLAG,
|
|
100
|
+
"typedef": TokenType.TYPEDEF,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
_SINGLE_CHARS = {
|
|
104
|
+
"{": TokenType.LBRACE,
|
|
105
|
+
"}": TokenType.RBRACE,
|
|
106
|
+
"[": TokenType.LBRACKET,
|
|
107
|
+
"]": TokenType.RBRACKET,
|
|
108
|
+
"(": TokenType.LPAREN,
|
|
109
|
+
")": TokenType.RPAREN,
|
|
110
|
+
";": TokenType.SEMICOLON,
|
|
111
|
+
",": TokenType.COMMA,
|
|
112
|
+
":": TokenType.COLON,
|
|
113
|
+
"?": TokenType.QUESTION,
|
|
114
|
+
"*": TokenType.STAR,
|
|
115
|
+
"=": TokenType.EQUALS,
|
|
116
|
+
"+": TokenType.PLUS,
|
|
117
|
+
"-": TokenType.MINUS,
|
|
118
|
+
"/": TokenType.SLASH,
|
|
119
|
+
"%": TokenType.PERCENT,
|
|
120
|
+
"&": TokenType.AMPERSAND,
|
|
121
|
+
"|": TokenType.PIPE,
|
|
122
|
+
"^": TokenType.CARET,
|
|
123
|
+
"~": TokenType.TILDE,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
_RE_IDENTIFIER = re.compile(r"[a-zA-Z_.][a-zA-Z0-9_.]*")
|
|
127
|
+
_RE_WHITESPACE = re.compile(r"[ \t\r\n]+")
|
|
128
|
+
|
|
129
|
+
IDENTIFIER_TYPES = set(_C_KEYWORDS.values()) | {TokenType.IDENTIFIER}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def tokenize(data: str) -> list[Token]:
|
|
133
|
+
"""Convenience function to tokenize input data."""
|
|
134
|
+
return Lexer(data).tokenize()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class Lexer:
|
|
138
|
+
"""Lexer compatible with C-like syntax for struct definitions and preprocessor directives."""
|
|
139
|
+
|
|
140
|
+
def __init__(self, data: str):
|
|
141
|
+
self.data = data
|
|
142
|
+
self._pos = 0
|
|
143
|
+
self._line = 1
|
|
144
|
+
self._column = 1
|
|
145
|
+
self._tokens: list[Token] = []
|
|
146
|
+
|
|
147
|
+
def reset(self) -> None:
|
|
148
|
+
"""Reset the lexer state for a new input."""
|
|
149
|
+
self._pos = 0
|
|
150
|
+
self._line = 1
|
|
151
|
+
self._column = 1
|
|
152
|
+
self._tokens = []
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def eof(self) -> bool:
|
|
156
|
+
"""Whether the end of the input has been reached."""
|
|
157
|
+
return self._pos >= len(self.data)
|
|
158
|
+
|
|
159
|
+
def _assert_not_eof(self) -> None:
|
|
160
|
+
"""Raise an error if EOF is reached."""
|
|
161
|
+
if self.eof:
|
|
162
|
+
raise self._error("unexpected end of input")
|
|
163
|
+
|
|
164
|
+
def _current(self) -> str:
|
|
165
|
+
"""Return the current character without consuming it. Raises an error if EOF is reached."""
|
|
166
|
+
self._assert_not_eof()
|
|
167
|
+
return self.data[self._pos]
|
|
168
|
+
|
|
169
|
+
def _get(self, start: int, end: int | None = None) -> str:
|
|
170
|
+
"""Get a slice of the input data from ``start`` to ``end``."""
|
|
171
|
+
if end is not None:
|
|
172
|
+
return self.data[start:end]
|
|
173
|
+
return self.data[start]
|
|
174
|
+
|
|
175
|
+
def _peek(self, offset: int = 1) -> str | None:
|
|
176
|
+
"""Peek at the character at the given offset without consuming it. Returns ``None`` if EOF is reached."""
|
|
177
|
+
return None if (idx := self._pos + offset) >= len(self.data) else self._get(idx)
|
|
178
|
+
|
|
179
|
+
def _take(self, num: int = 1) -> str:
|
|
180
|
+
"""Consume and return the next ``num`` characters, updating line and column counters."""
|
|
181
|
+
end = self._pos + num
|
|
182
|
+
if end > len(self.data):
|
|
183
|
+
self._pos = len(self.data)
|
|
184
|
+
self._assert_not_eof()
|
|
185
|
+
|
|
186
|
+
if num == 1:
|
|
187
|
+
result = self.data[self._pos]
|
|
188
|
+
self._pos = end
|
|
189
|
+
if result == "\n":
|
|
190
|
+
self._line += 1
|
|
191
|
+
self._column = 1
|
|
192
|
+
else:
|
|
193
|
+
self._column += 1
|
|
194
|
+
return result
|
|
195
|
+
|
|
196
|
+
result = self.data[self._pos : end]
|
|
197
|
+
self._pos = end
|
|
198
|
+
|
|
199
|
+
if num_newlines := result.count("\n"):
|
|
200
|
+
self._line += num_newlines
|
|
201
|
+
self._column = len(result) - result.rfind("\n")
|
|
202
|
+
else:
|
|
203
|
+
self._column += num
|
|
204
|
+
|
|
205
|
+
return result
|
|
206
|
+
|
|
207
|
+
def _expect(self, *chars: str) -> str:
|
|
208
|
+
"""Consume the expected characters or raise an error."""
|
|
209
|
+
if self._current() not in chars:
|
|
210
|
+
actual = "end of input" if self.eof else repr(self._current())
|
|
211
|
+
expected = " or ".join(repr(c) for c in chars)
|
|
212
|
+
raise self._error(f"expected {expected}, got {actual}")
|
|
213
|
+
|
|
214
|
+
return self._take()
|
|
215
|
+
|
|
216
|
+
def _error(self, msg: str, *, line: int | None = None) -> LexerError:
|
|
217
|
+
return LexerError(f"line {line if line is not None else self._line}: {msg}")
|
|
218
|
+
|
|
219
|
+
def _emit(self, type: TokenType, value: str, line: int, column: int = 0) -> None:
|
|
220
|
+
"""Emit a token with the given type and value at the specified line and column."""
|
|
221
|
+
self._tokens.append(Token(type, value, line, column))
|
|
222
|
+
|
|
223
|
+
def _read_conditional(
|
|
224
|
+
self, condition: str | Callable[[str], bool], *, or_eof: bool = True, invert: bool = False
|
|
225
|
+
) -> str:
|
|
226
|
+
"""Read while or until the current character matches the condition.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
condition: A function that returns ``True`` to stop.
|
|
230
|
+
or_eof: If True, also stop if EOF is reached. If False, EOF will not stop the read and will raise an error.
|
|
231
|
+
invert: If True, invert the condition (i.e. read while it matches instead of until it matches).
|
|
232
|
+
"""
|
|
233
|
+
start = self._pos
|
|
234
|
+
while True:
|
|
235
|
+
if self.eof:
|
|
236
|
+
if not or_eof:
|
|
237
|
+
self._assert_not_eof()
|
|
238
|
+
break
|
|
239
|
+
|
|
240
|
+
ch = self.data[self._pos]
|
|
241
|
+
if isinstance(condition, str):
|
|
242
|
+
if (ch in condition) != invert:
|
|
243
|
+
break
|
|
244
|
+
else:
|
|
245
|
+
if condition(ch) != invert:
|
|
246
|
+
break
|
|
247
|
+
|
|
248
|
+
self._pos += 1
|
|
249
|
+
|
|
250
|
+
end = self._pos
|
|
251
|
+
self._pos = start
|
|
252
|
+
return self._take(end - start)
|
|
253
|
+
|
|
254
|
+
def _read_until(self, condition: str | Callable[[str], bool], *, or_eof: bool = True) -> str:
|
|
255
|
+
"""Read until the current character matches the condition.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
condition: Characters to match, or a function that returns ``True`` to stop.
|
|
259
|
+
or_eof: If True, also stop if EOF is reached. If False, EOF will not stop the read and will raise an error.
|
|
260
|
+
"""
|
|
261
|
+
return self._read_conditional(condition, or_eof=or_eof)
|
|
262
|
+
|
|
263
|
+
def _read_while(self, condition: str | Callable[[str], bool], *, or_eof: bool = True) -> str:
|
|
264
|
+
"""Read while the current character matches the condition.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
condition: Characters to match, or a function that returns ``True`` to continue.
|
|
268
|
+
or_eof: If True, also stop if EOF is reached. If False, EOF will not stop the read and will raise an error.
|
|
269
|
+
"""
|
|
270
|
+
return self._read_conditional(condition, or_eof=or_eof, invert=True)
|
|
271
|
+
|
|
272
|
+
def _skip_whitespace(self) -> None:
|
|
273
|
+
"""Skip whitespace characters."""
|
|
274
|
+
if match := _RE_WHITESPACE.match(self.data, self._pos):
|
|
275
|
+
self._take(match.end() - self._pos)
|
|
276
|
+
|
|
277
|
+
def _skip_comment(self) -> None:
|
|
278
|
+
"""Skip a comment starting at the current position, if present."""
|
|
279
|
+
if self._current() == "/":
|
|
280
|
+
peek = self._peek()
|
|
281
|
+
|
|
282
|
+
if peek == "*":
|
|
283
|
+
self._take(2) # Consume /*
|
|
284
|
+
end = self.data.find("*/", self._pos)
|
|
285
|
+
if end != -1:
|
|
286
|
+
self._take(end - self._pos + 2)
|
|
287
|
+
else:
|
|
288
|
+
self._take(len(self.data) - self._pos)
|
|
289
|
+
|
|
290
|
+
elif peek == "/":
|
|
291
|
+
self._take(2) # Consume //
|
|
292
|
+
end = self.data.find("\n", self._pos)
|
|
293
|
+
if end != -1:
|
|
294
|
+
self._take(end - self._pos)
|
|
295
|
+
else:
|
|
296
|
+
self._take(len(self.data) - self._pos)
|
|
297
|
+
|
|
298
|
+
def _skip_whitespace_and_comments(self) -> None:
|
|
299
|
+
"""Skip whitespace and comments."""
|
|
300
|
+
while True:
|
|
301
|
+
start_pos = self._pos
|
|
302
|
+
self._skip_whitespace()
|
|
303
|
+
if self.eof:
|
|
304
|
+
break
|
|
305
|
+
self._skip_comment()
|
|
306
|
+
if self.eof or self._pos == start_pos:
|
|
307
|
+
break
|
|
308
|
+
|
|
309
|
+
def _read_identifier(self) -> str:
|
|
310
|
+
"""Read an identifier starting with a letter or underscore, followed by letters, digits, or underscores."""
|
|
311
|
+
if match := _RE_IDENTIFIER.match(self.data, self._pos):
|
|
312
|
+
return self._take(match.end() - self._pos)
|
|
313
|
+
return ""
|
|
314
|
+
|
|
315
|
+
def _read_number(self) -> str:
|
|
316
|
+
"""Read a numeric literal, supporting decimal, hex (0x), octal (0, 0o), binary (0b), and C-style suffixes."""
|
|
317
|
+
start = self._pos
|
|
318
|
+
is_float = False
|
|
319
|
+
|
|
320
|
+
if self._current() == "0" and self._peek() in ("x", "X", "b", "B", "o", "O"):
|
|
321
|
+
self._expect("0") # Consume leading 0
|
|
322
|
+
suffix = self._take().lower()
|
|
323
|
+
|
|
324
|
+
if suffix == "x" and not self._read_while("0123456789abcdefABCDEF"):
|
|
325
|
+
raise self._error("invalid hexadecimal literal")
|
|
326
|
+
|
|
327
|
+
if suffix == "b" and not self._read_while("01"):
|
|
328
|
+
raise self._error("invalid binary literal")
|
|
329
|
+
|
|
330
|
+
if suffix == "o" and not self._read_while("01234567"):
|
|
331
|
+
raise self._error("invalid octal literal")
|
|
332
|
+
|
|
333
|
+
else:
|
|
334
|
+
# Consume decimal/octal digits
|
|
335
|
+
self._read_while("0123456789")
|
|
336
|
+
|
|
337
|
+
# Decimal point for float literals
|
|
338
|
+
if self._peek(0) == ".":
|
|
339
|
+
is_float = True
|
|
340
|
+
self._take()
|
|
341
|
+
self._read_while("0123456789")
|
|
342
|
+
|
|
343
|
+
raw = self._get(start, self._pos)
|
|
344
|
+
|
|
345
|
+
if not is_float:
|
|
346
|
+
# Strip C-style suffixes (ULL, ull, ul, u, l, ll, etc.)
|
|
347
|
+
self._read_while("uUlL")
|
|
348
|
+
|
|
349
|
+
# Convert octal: leading 0 without 0x/0b → insert 'o'
|
|
350
|
+
if len(raw) > 1 and raw[0] == "0" and raw[1].lower() not in ("x", "b", "o"):
|
|
351
|
+
if raw[1] not in "01234567":
|
|
352
|
+
raise self._error("invalid octal literal")
|
|
353
|
+
raw = raw[0] + "o" + raw[1:]
|
|
354
|
+
|
|
355
|
+
return raw
|
|
356
|
+
|
|
357
|
+
def _read_string(self) -> str:
|
|
358
|
+
"""Read a quoted string."""
|
|
359
|
+
quote = self._expect('"', "'") # Consume opening quote
|
|
360
|
+
start = self._pos
|
|
361
|
+
|
|
362
|
+
while not self.eof:
|
|
363
|
+
ch = self.data[self._pos]
|
|
364
|
+
if ch == "\\":
|
|
365
|
+
if self._pos + 1 < len(self.data):
|
|
366
|
+
self._pos += 2
|
|
367
|
+
else:
|
|
368
|
+
self._pos += 1
|
|
369
|
+
continue
|
|
370
|
+
|
|
371
|
+
if ch == quote:
|
|
372
|
+
break
|
|
373
|
+
|
|
374
|
+
self._pos += 1
|
|
375
|
+
|
|
376
|
+
end = self._pos
|
|
377
|
+
self._pos = start
|
|
378
|
+
result = self._take(end - start)
|
|
379
|
+
self._expect(quote) # Consume closing quote
|
|
380
|
+
|
|
381
|
+
return result
|
|
382
|
+
|
|
383
|
+
def _read_preprocessor(self) -> None:
|
|
384
|
+
"""Read a preprocessor directive starting with ``#``."""
|
|
385
|
+
line = self._line
|
|
386
|
+
col = self._column
|
|
387
|
+
self._expect("#") # Consume `#`
|
|
388
|
+
|
|
389
|
+
# Check for `#[flags]`
|
|
390
|
+
if self._current() == "[":
|
|
391
|
+
self._expect("[") # Consume `[`
|
|
392
|
+
value = self._read_until("]")
|
|
393
|
+
|
|
394
|
+
self._assert_not_eof()
|
|
395
|
+
self._expect("]") # Consume `]`
|
|
396
|
+
|
|
397
|
+
self._emit(TokenType.PP_FLAGS, value, line, col)
|
|
398
|
+
return
|
|
399
|
+
|
|
400
|
+
# Read the keyword after #
|
|
401
|
+
self._skip_whitespace_and_comments()
|
|
402
|
+
keyword = self._read_identifier()
|
|
403
|
+
|
|
404
|
+
if (token_type := _PP_KEYWORDS.get(keyword)) is None:
|
|
405
|
+
raise self._error(f"unknown preprocessor directive '#{keyword}'", line=line)
|
|
406
|
+
|
|
407
|
+
self._emit(token_type, keyword, line, col)
|
|
408
|
+
|
|
409
|
+
if token_type == TokenType.PP_DEFINE:
|
|
410
|
+
self._skip_whitespace_and_comments()
|
|
411
|
+
|
|
412
|
+
if not (name := self._read_identifier()):
|
|
413
|
+
raise self._error("expected identifier after '#define'", line=line)
|
|
414
|
+
self._emit(TokenType.IDENTIFIER, name, line)
|
|
415
|
+
|
|
416
|
+
self._skip_whitespace_and_comments()
|
|
417
|
+
if self.eof or self._line != line:
|
|
418
|
+
# No value, just a simple macro definition
|
|
419
|
+
return
|
|
420
|
+
|
|
421
|
+
if (value := self._read_until("\n")).strip():
|
|
422
|
+
self._emit(TokenType.STRING, value, line)
|
|
423
|
+
|
|
424
|
+
elif token_type == TokenType.PP_INCLUDE:
|
|
425
|
+
# Read include path — either "..." or <...>
|
|
426
|
+
self._skip_whitespace_and_comments()
|
|
427
|
+
|
|
428
|
+
ch = self._current()
|
|
429
|
+
if ch == '"' or ch == "'":
|
|
430
|
+
value = self._read_string()
|
|
431
|
+
elif ch == "<":
|
|
432
|
+
self._expect("<") # Consume `<`
|
|
433
|
+
value = self._read_until(">", or_eof=False)
|
|
434
|
+
self._expect(">") # Consume closing `>`
|
|
435
|
+
value = f"<{value}>"
|
|
436
|
+
else:
|
|
437
|
+
raise self._error("expected include path after '#include'", line=line)
|
|
438
|
+
|
|
439
|
+
self._emit(TokenType.STRING, value, line)
|
|
440
|
+
|
|
441
|
+
def tokenize(self) -> list[Token]:
|
|
442
|
+
"""Tokenize the input data and return a list of tokens."""
|
|
443
|
+
while not self.eof:
|
|
444
|
+
self._skip_whitespace_and_comments()
|
|
445
|
+
if self.eof:
|
|
446
|
+
break
|
|
447
|
+
|
|
448
|
+
ch = self._current()
|
|
449
|
+
line = self._line
|
|
450
|
+
col = self._column
|
|
451
|
+
|
|
452
|
+
if ch == "#":
|
|
453
|
+
# C-style preprocessor directive
|
|
454
|
+
self._read_preprocessor()
|
|
455
|
+
|
|
456
|
+
elif ch in ('"', "'"):
|
|
457
|
+
self._emit(TokenType.STRING, self._read_string(), line, col)
|
|
458
|
+
|
|
459
|
+
elif ch in ("b", "B") and self._peek() in ("'", '"'):
|
|
460
|
+
# Binary string literal like `b"..."` or `b'...'`
|
|
461
|
+
self._take() # Consume `b`
|
|
462
|
+
self._emit(TokenType.BYTES, f"b{self._read_string()!r}", line, col)
|
|
463
|
+
|
|
464
|
+
elif ch.isdigit():
|
|
465
|
+
self._emit(TokenType.NUMBER, self._read_number(), line, col)
|
|
466
|
+
|
|
467
|
+
elif ch.isalpha() or ch == "_":
|
|
468
|
+
ident = self._read_identifier()
|
|
469
|
+
token_type = _C_KEYWORDS.get(ident, TokenType.IDENTIFIER)
|
|
470
|
+
self._emit(token_type, ident, line, col)
|
|
471
|
+
|
|
472
|
+
elif ch == "<" and self._peek() == "<":
|
|
473
|
+
self._emit(TokenType.LSHIFT, self._take(2), line, col)
|
|
474
|
+
|
|
475
|
+
elif ch == ">" and self._peek() == ">":
|
|
476
|
+
self._emit(TokenType.RSHIFT, self._take(2), line, col)
|
|
477
|
+
|
|
478
|
+
elif ch == "-" and (
|
|
479
|
+
self._pos == 0
|
|
480
|
+
or self._tokens[-1].type
|
|
481
|
+
not in (TokenType.IDENTIFIER, TokenType.NUMBER, TokenType.RPAREN, TokenType.RBRACKET)
|
|
482
|
+
):
|
|
483
|
+
self._emit(TokenType.UNARY_MINUS, self._take(), line, col)
|
|
484
|
+
|
|
485
|
+
elif ch in _SINGLE_CHARS:
|
|
486
|
+
self._emit(_SINGLE_CHARS[ch], self._take(), line, col)
|
|
487
|
+
|
|
488
|
+
else:
|
|
489
|
+
raise self._error(f"unexpected character {ch!r}", line=line)
|
|
490
|
+
|
|
491
|
+
self._emit(TokenType.EOF, "", self._line, self._column)
|
|
492
|
+
return self._tokens
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
class TokenCursor:
|
|
496
|
+
"""Shared token cursor helpers for parsers using ``Token`` streams."""
|
|
497
|
+
|
|
498
|
+
def __init__(self, tokens: list[Token] | None = None):
|
|
499
|
+
self._tokens: list[Token] = tokens or []
|
|
500
|
+
self._pos = 0
|
|
501
|
+
|
|
502
|
+
def _reset_tokens(self, tokens: list[Token]) -> None:
|
|
503
|
+
"""Replace the current token stream and reset position."""
|
|
504
|
+
self._tokens = tokens
|
|
505
|
+
self._pos = 0
|
|
506
|
+
|
|
507
|
+
def _reset_cursor(self) -> None:
|
|
508
|
+
"""Reset only the cursor position while keeping the current tokens."""
|
|
509
|
+
self._pos = 0
|
|
510
|
+
|
|
511
|
+
def _assert_not_eof(self) -> None:
|
|
512
|
+
"""Raise an error if EOF is reached."""
|
|
513
|
+
if self._tokens[self._pos].type == TokenType.EOF:
|
|
514
|
+
raise self._error("unexpected end of input", token=self._tokens[self._pos])
|
|
515
|
+
|
|
516
|
+
def _previous(self) -> Token:
|
|
517
|
+
"""Return the previous token without consuming it."""
|
|
518
|
+
return self._tokens[self._pos - 1] if self._pos > 0 else Token(TokenType.EOF, "", 0)
|
|
519
|
+
|
|
520
|
+
def _current(self) -> Token:
|
|
521
|
+
"""Return the current token without consuming it."""
|
|
522
|
+
return self._tokens[self._pos]
|
|
523
|
+
|
|
524
|
+
def _peek(self, offset: int = 1) -> Token:
|
|
525
|
+
"""Peek at a token at the given offset without consuming it. Returns EOF on overflow."""
|
|
526
|
+
idx = self._pos + offset
|
|
527
|
+
return self._tokens[-1] if idx >= len(self._tokens) else self._tokens[idx]
|
|
528
|
+
|
|
529
|
+
def _take(self) -> Token:
|
|
530
|
+
"""Consume and return the current token."""
|
|
531
|
+
token = self._current()
|
|
532
|
+
if token.type != TokenType.EOF:
|
|
533
|
+
self._pos += 1
|
|
534
|
+
return token
|
|
535
|
+
|
|
536
|
+
def _expect(self, *types: TokenType) -> Token:
|
|
537
|
+
"""Consume and return the current token if it matches, otherwise raise an error."""
|
|
538
|
+
token = self._current()
|
|
539
|
+
if token.type not in types:
|
|
540
|
+
actual = "end of input" if token.type == TokenType.EOF else token.value
|
|
541
|
+
expected = " or ".join(t.value for t in types)
|
|
542
|
+
expected = f"one of {expected}" if len(types) > 1 else expected
|
|
543
|
+
raise self._error(f"expected {expected}, got {actual}")
|
|
544
|
+
return self._take()
|
|
545
|
+
|
|
546
|
+
def _collect_until(self, *terminators: TokenType) -> str:
|
|
547
|
+
"""Collect token values until one of the terminators is reached, tracking nesting."""
|
|
548
|
+
terminators_set = set(terminators)
|
|
549
|
+
start = self._current()
|
|
550
|
+
parts: list[str] = []
|
|
551
|
+
depth = 0
|
|
552
|
+
|
|
553
|
+
while (token := self._current()).type != TokenType.EOF:
|
|
554
|
+
if depth == 0 and token.type in terminators_set:
|
|
555
|
+
break
|
|
556
|
+
|
|
557
|
+
if token.type in (TokenType.LPAREN, TokenType.LBRACKET):
|
|
558
|
+
depth += 1
|
|
559
|
+
elif token.type in (TokenType.RPAREN, TokenType.RBRACKET):
|
|
560
|
+
depth -= 1
|
|
561
|
+
if depth < 0:
|
|
562
|
+
raise self._error("unmatched closing bracket", token=token)
|
|
563
|
+
|
|
564
|
+
parts.append(token.value)
|
|
565
|
+
self._pos += 1
|
|
566
|
+
|
|
567
|
+
if depth > 0:
|
|
568
|
+
raise self._error("unclosed opening bracket", token=start)
|
|
569
|
+
|
|
570
|
+
return " ".join(parts)
|
|
571
|
+
|
|
572
|
+
def _error(self, msg: str, *, token: Token | None = None) -> Exception:
|
|
573
|
+
"""Subclasses should override to return an appropriate exception."""
|
|
574
|
+
raise NotImplementedError
|