dissect.cstruct 4.8.dev9__py3-none-any.whl → 5.0.dev6__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.
Files changed (31) hide show
  1. dissect/cstruct/__init__.py +2 -2
  2. dissect/cstruct/bitbuffer.py +3 -3
  3. dissect/cstruct/compiler.py +7 -7
  4. dissect/cstruct/cstruct.py +196 -75
  5. dissect/cstruct/{exceptions.py → exception.py} +4 -4
  6. dissect/cstruct/expression.py +188 -271
  7. dissect/cstruct/lexer.py +584 -0
  8. dissect/cstruct/parser.py +444 -593
  9. dissect/cstruct/tools/stubgen.py +18 -22
  10. dissect/cstruct/types/base.py +74 -31
  11. dissect/cstruct/types/char.py +12 -8
  12. dissect/cstruct/types/enum.py +33 -13
  13. dissect/cstruct/types/int.py +9 -7
  14. dissect/cstruct/types/leb128.py +6 -4
  15. dissect/cstruct/types/packed.py +19 -10
  16. dissect/cstruct/types/pointer.py +74 -56
  17. dissect/cstruct/types/structure.py +128 -28
  18. dissect/cstruct/types/void.py +6 -4
  19. dissect/cstruct/types/wchar.py +19 -14
  20. dissect/cstruct/{utils.py → util.py} +60 -35
  21. {dissect_cstruct-4.8.dev9.dist-info → dissect_cstruct-5.0.dev6.dist-info}/METADATA +1 -1
  22. dissect_cstruct-5.0.dev6.dist-info/RECORD +32 -0
  23. {dissect_cstruct-4.8.dev9.dist-info → dissect_cstruct-5.0.dev6.dist-info}/scm_file_list.json +4 -2
  24. dissect_cstruct-5.0.dev6.dist-info/scm_version.json +8 -0
  25. dissect_cstruct-4.8.dev9.dist-info/RECORD +0 -31
  26. dissect_cstruct-4.8.dev9.dist-info/scm_version.json +0 -8
  27. {dissect_cstruct-4.8.dev9.dist-info → dissect_cstruct-5.0.dev6.dist-info}/WHEEL +0 -0
  28. {dissect_cstruct-4.8.dev9.dist-info → dissect_cstruct-5.0.dev6.dist-info}/entry_points.txt +0 -0
  29. {dissect_cstruct-4.8.dev9.dist-info → dissect_cstruct-5.0.dev6.dist-info}/licenses/COPYRIGHT +0 -0
  30. {dissect_cstruct-4.8.dev9.dist-info → dissect_cstruct-5.0.dev6.dist-info}/licenses/LICENSE +0 -0
  31. {dissect_cstruct-4.8.dev9.dist-info → dissect_cstruct-5.0.dev6.dist-info}/top_level.txt +0 -0
dissect/cstruct/parser.py CHANGED
@@ -2,23 +2,25 @@ from __future__ import annotations
2
2
 
3
3
  import ast
4
4
  import re
5
- from typing import TYPE_CHECKING, Any
5
+ from typing import TYPE_CHECKING
6
6
 
7
7
  from dissect.cstruct import compiler
8
- from dissect.cstruct.exceptions import (
8
+ from dissect.cstruct.exception import (
9
9
  ExpressionParserError,
10
- ExpressionTokenizerError,
10
+ LexerError,
11
11
  ParserError,
12
12
  )
13
13
  from dissect.cstruct.expression import Expression
14
- from dissect.cstruct.types import BaseArray, Field, Structure
14
+ from dissect.cstruct.lexer import IDENTIFIER_TYPES, TokenCursor, TokenType, format_error_context, tokenize
15
+ from dissect.cstruct.types import BaseArray, BaseType, Field, Structure
15
16
 
16
17
  if TYPE_CHECKING:
17
18
  from dissect.cstruct import cstruct
18
- from dissect.cstruct.types import BaseType
19
+ from dissect.cstruct.lexer import Token
20
+ from dissect.cstruct.types import BaseType, Enum, Flag
19
21
 
20
22
 
21
- class Parser:
23
+ class Parser(TokenCursor):
22
24
  """Base class for definition parsers.
23
25
 
24
26
  Args:
@@ -26,688 +28,537 @@ class Parser:
26
28
  """
27
29
 
28
30
  def __init__(self, cs: cstruct):
29
- self.cstruct = cs
31
+ super().__init__()
32
+ self.cs = cs
30
33
 
31
34
  def parse(self, data: str) -> None:
32
35
  """This function should parse definitions to cstruct types.
33
36
 
34
37
  Args:
35
- data: Data to parse definitions from, usually a string.
38
+ data: Data to parse definitions from.
36
39
  """
37
40
  raise NotImplementedError
38
41
 
39
42
 
40
- class TokenParser(Parser):
41
- """Definition parser for C-like structure syntax.
43
+ def _join_line_continuations(string: str) -> str:
44
+ # Join lines ending with backslash
45
+ return re.sub(r"\\\n", "", string)
46
+
47
+
48
+ class CStyleParser(Parser):
49
+ """Recursive descent parser for C-like structure definitions.
42
50
 
43
51
  Args:
44
52
  cs: An instance of cstruct.
45
53
  compiled: Whether structs should be compiled or not.
54
+ align: Whether to use aligned struct reads.
46
55
  """
47
56
 
48
57
  def __init__(self, cs: cstruct, compiled: bool = True, align: bool = False):
49
58
  super().__init__(cs)
50
-
51
59
  self.compiled = compiled
52
60
  self.align = align
53
- self.TOK = self._tokencollection()
54
- self._conditionals = []
55
- self._conditionals_depth = 0
56
-
57
- @staticmethod
58
- def _tokencollection() -> TokenCollection:
59
- TOK = TokenCollection()
60
- TOK.add(r"#\[(?P<values>[^\]]+)\](?=\s*)", "CONFIG_FLAG")
61
- TOK.add(r"#define\s+(?P<name>[^\s]+)(?P<value>[^\r\n]*)", "DEFINE")
62
- TOK.add(r"#undef\s+(?P<name>[^\s]+)\s*", "UNDEF")
63
- TOK.add(r"#ifdef\s+(?P<name>[^\s]+)\s*", "IFDEF")
64
- TOK.add(r"#ifndef\s+(?P<name>[^\s]+)\s*", "IFNDEF")
65
- TOK.add(r"#else\s*", "ELSE")
66
- TOK.add(r"#endif\s*", "ENDIF")
67
- TOK.add(r"typedef(?=\s)", "TYPEDEF")
68
- TOK.add(r"(?:struct|union)(?=\s|{)", "STRUCT")
69
- TOK.add(
70
- r"(?P<enumtype>enum|flag)\s+(?P<name>[^\s:{]+)?\s*(:\s"
71
- r"*(?P<type>[^{]+?)\s*)?\{(?P<values>[^}]+)\}\s*(?=;)",
72
- "ENUM",
73
- )
74
- TOK.add(r"(?<=})\s*(?P<defs>(?:[a-zA-Z0-9_]+\s*,\s*)+[a-zA-Z0-9_]+)\s*(?=;)", "DEFS")
75
- TOK.add(r"(?P<name>\**?\s*[a-zA-Z0-9_]+)(?:\s*:\s*(?P<bits>\d+))?(?:\[(?P<count>[^;]*)\])?\s*(?=;)", "NAME")
76
- TOK.add(r"#include\s+(?P<name>[^\s]+)\s*", "INCLUDE")
77
- TOK.add(r"[a-zA-Z_][a-zA-Z0-9_]*", "IDENTIFIER")
78
- TOK.add(r"[{}]", "BLOCK")
79
- TOK.add(r"\$(?P<name>[^\s]+) = (?P<value>{[^}]+})\w*[\r\n]+", "LOOKUP")
80
- TOK.add(r";", "EOL")
81
- TOK.add(r"\s+", None)
82
- TOK.add(r".", None)
83
-
84
- return TOK
85
-
86
- def _identifier(self, tokens: TokenConsumer) -> str:
87
- idents = []
88
- while tokens.next == self.TOK.IDENTIFIER:
89
- idents.append(tokens.consume())
90
- return " ".join([i.value for i in idents])
91
-
92
- def _conditional(self, tokens: TokenConsumer) -> None:
93
- token = tokens.consume()
94
- pattern = self.TOK.patterns[token.token]
95
- match = pattern.match(token.value).groupdict()
96
-
97
- value = match["name"]
98
-
99
- if token.token == self.TOK.IFDEF:
100
- self._conditionals.append(value in self.cstruct.consts)
101
- elif token.token == self.TOK.IFNDEF:
102
- self._conditionals.append(value not in self.cstruct.consts)
103
-
104
- def _check_conditional(self, tokens: TokenConsumer) -> bool:
105
- """Check and handle conditionals. Return a boolean indicating if we need to continue to the next token."""
106
- if self._conditionals and self._conditionals_depth == len(self._conditionals):
107
- # If we have a conditional and the depth matches, handle it accordingly
108
- if tokens.next == self.TOK.ELSE:
109
- # Flip the last conditional
110
- tokens.consume()
111
- self._conditionals[-1] = not self._conditionals[-1]
112
- return True
113
-
114
- if tokens.next == self.TOK.ENDIF:
115
- # Pop the last conditional
116
- tokens.consume()
117
- self._conditionals.pop()
118
- self._conditionals_depth -= 1
119
- return True
120
-
121
- if tokens.next in (self.TOK.IFDEF, self.TOK.IFNDEF):
122
- # If we encounter a new conditional, increase the depth
123
- self._conditionals_depth += 1
124
-
125
- if tokens.next == self.TOK.ENDIF:
126
- # Similarly, decrease the depth if needed
127
- self._conditionals_depth -= 1
128
-
129
- if self._conditionals and not self._conditionals[-1]:
130
- # If the last conditional evaluated to False, skip the next token
131
- tokens.consume()
132
- return True
61
+ self._data = None
133
62
 
134
- if tokens.next in (self.TOK.IFDEF, self.TOK.IFNDEF):
135
- # If the next token is a conditional, process it
136
- self._conditional(tokens)
137
- return True
138
-
139
- return False
63
+ self._flags: list[str] = []
64
+ self._conditional_stack: list[tuple[Token, bool]] = []
140
65
 
141
- def _constant(self, tokens: TokenConsumer) -> None:
142
- const = tokens.consume()
143
- pattern = self.TOK.patterns[self.TOK.DEFINE]
144
- match = pattern.match(const.value).groupdict()
66
+ def reset(self) -> None:
67
+ """Reset the parser state for a new input."""
68
+ self._reset_tokens([])
69
+ self._flags = []
70
+ self._conditional_stack = []
145
71
 
146
- value = match["value"].strip()
147
- try:
148
- value = ast.literal_eval(value)
149
- except (ValueError, SyntaxError):
150
- pass
72
+ def parse(self, data: str) -> None:
73
+ """Parse C-like definitions from the input data."""
74
+ self.reset()
151
75
 
152
- if isinstance(value, str):
153
- try:
154
- value = Expression(value).evaluate(self.cstruct)
155
- except (ExpressionParserError, ExpressionTokenizerError):
156
- pass
76
+ data = _join_line_continuations(data)
157
77
 
158
- self.cstruct.consts[match["name"]] = value
78
+ # Keep a reference for error messages
79
+ self._data = data
80
+ self._reset_tokens(tokenize(data))
81
+ self._parse()
159
82
 
160
- def _undef(self, tokens: TokenConsumer) -> None:
161
- const = tokens.consume()
162
- pattern = self.TOK.patterns[self.TOK.UNDEF]
163
- match = pattern.match(const.value).groupdict()
83
+ def _match(self, *types: TokenType) -> Token | None:
84
+ """Consume and return the current token if it matches any of the given types, otherwise return None."""
85
+ if self._current().type in types:
86
+ return self._take()
87
+ return None
164
88
 
165
- if match["name"] in self.cstruct.consts:
166
- del self.cstruct.consts[match["name"]]
167
- else:
168
- raise ParserError(f"line {self._lineno(const)}: constant {match['name']!r} not defined")
89
+ def _at(self, *types: TokenType) -> bool:
90
+ """Return whether the current token matches any of the given types."""
91
+ return self._tokens[self._pos].type in types
169
92
 
170
- def _enum(self, tokens: TokenConsumer) -> None:
171
- # We cheat with enums because the entire enum is in the token
172
- etok = tokens.consume()
93
+ def _error(self, msg: str, *, token: Token | None = None) -> ParserError:
94
+ lineno = (token if token is not None else self._tokens[self._pos]).line
95
+ if self._data is None:
96
+ return ParserError(f"line {lineno}: {msg}")
173
97
 
174
- pattern = self.TOK.patterns[self.TOK.ENUM]
175
- # Dirty trick because the regex expects a ; but we don't want it to be part of the value
176
- d = pattern.match(etok.value + ";").groupdict()
177
- enumtype = d["enumtype"]
98
+ content = format_error_context(self._data, lineno)
99
+ return ParserError(f"line {lineno}: {msg}\n{content}")
178
100
 
179
- nextval = 0
180
- if enumtype == "flag":
181
- nextval = 1
101
+ def _in_false_branch(self) -> bool:
102
+ """Return whether we're currently in a false conditional branch."""
103
+ return bool(self._conditional_stack) and not self._conditional_stack[-1][1]
182
104
 
183
- values = {}
184
- for line in d["values"].splitlines():
185
- for v in line.split(","):
186
- key, _, val = v.partition("=")
187
- key = key.strip()
188
- val = val.strip()
189
- if not key:
190
- continue
105
+ def _handle_preprocessor(self) -> bool:
106
+ """Handle preprocessor directives.
191
107
 
192
- val = nextval if not val else Expression(val).evaluate(self.cstruct, values)
108
+ Returns ``True`` if a directive was processed, indicating the caller should continue its loop.
109
+ """
110
+ token = self._current()
193
111
 
194
- if enumtype == "flag":
195
- high_bit = val.bit_length() - 1
196
- nextval = 2 ** (high_bit + 1)
197
- else:
198
- nextval = val + 1
112
+ if token.type in (TokenType.PP_IFDEF, TokenType.PP_IFNDEF, TokenType.PP_ELSE, TokenType.PP_ENDIF):
113
+ self._handle_conditional()
114
+ return True
199
115
 
200
- values[key] = val
116
+ if token.type == TokenType.PP_DEFINE:
117
+ self._parse_define()
118
+ return True
201
119
 
202
- if not d["type"]:
203
- d["type"] = "uint32"
120
+ if token.type == TokenType.PP_UNDEF:
121
+ self._parse_undef()
122
+ return True
204
123
 
205
- factory = self.cstruct._make_flag if enumtype == "flag" else self.cstruct._make_enum
124
+ if token.type == TokenType.PP_INCLUDE:
125
+ self._parse_include()
126
+ return True
206
127
 
207
- enum = factory(d["name"] or "", self.cstruct.resolve(d["type"]), values)
208
- if not enum.__name__:
209
- self.cstruct.consts.update(enum.__members__)
210
- else:
211
- self.cstruct.add_type(enum.__name__, enum)
128
+ return False
212
129
 
213
- tokens.eol()
130
+ def _parse(self) -> None:
131
+ """Parse top-level definitions from the token stream."""
132
+ while (token := self._current()).type != TokenType.EOF:
133
+ if self._handle_preprocessor():
134
+ continue
214
135
 
215
- def _typedef(self, tokens: TokenConsumer) -> None:
216
- tokens.consume()
217
- type_ = None
136
+ if token.type == TokenType.PP_FLAGS:
137
+ self._parse_config_flags()
138
+ elif token.type == TokenType.TYPEDEF:
139
+ self._parse_typedef()
140
+ elif token.type in (TokenType.STRUCT, TokenType.UNION):
141
+ self._parse_struct_or_union()
218
142
 
219
- names = []
143
+ # Skip variable declarations after struct/union definitions
144
+ while not self._at(TokenType.SEMICOLON, TokenType.EOF):
145
+ self._pos += 1
220
146
 
221
- if tokens.next == self.TOK.IDENTIFIER:
222
- type_ = self.cstruct.resolve(self._identifier(tokens))
223
- elif tokens.next == self.TOK.STRUCT:
224
- type_ = self._struct(tokens)
225
- if not type_.__anonymous__:
226
- names.append(type_.__name__)
147
+ self._expect(TokenType.SEMICOLON)
148
+ elif token.type in (TokenType.ENUM, TokenType.FLAG):
149
+ type_ = self._parse_enum_or_flag()
227
150
 
228
- names.extend(self._names(tokens))
229
- for name in names:
230
- if issubclass(type_, Structure) and type_.__anonymous__:
231
- type_.__anonymous__ = False
232
- type_.__name__ = name
233
- type_.__qualname__ = name
151
+ # If it's an anonymous enum/flag, add its members to the constants for convenience
152
+ if not type_.__name__:
153
+ for k, v in type_.__members__.items():
154
+ self.cs.add_const(k, v)
234
155
 
235
- type_, name, bits = self._parse_field_type(type_, name)
236
- if bits is not None:
237
- raise ParserError(f"line {self._lineno(tokens.previous)}: typedefs cannot have bitfields")
238
- self.cstruct.add_type(name, type_)
239
-
240
- def _struct(self, tokens: TokenConsumer, register: bool = False) -> type[Structure]:
241
- stype = tokens.consume()
242
-
243
- factory = self.cstruct._make_union if stype.value.startswith("union") else self.cstruct._make_struct
244
-
245
- st = None
246
- names = []
247
- registered = False
248
-
249
- if tokens.next == self.TOK.IDENTIFIER:
250
- ident = tokens.consume()
251
- if register:
252
- # Pre-register an empty struct for self-referencing
253
- # We update this instance later with the fields
254
- st = factory(ident.value, [], align=self.align)
255
- if self.compiled and "nocompile" not in tokens.flags:
256
- st = compiler.compile(st)
257
- self.cstruct.add_type(ident.value, st)
258
- registered = True
156
+ self._expect(TokenType.SEMICOLON)
259
157
  else:
260
- names.append(ident.value)
261
-
262
- if tokens.next == self.TOK.NAME:
263
- # As part of a struct field
264
- # struct type_name field_name;
265
- if not names:
266
- raise ParserError(f"line {self._lineno(tokens.next)}: unexpected anonymous struct")
267
- return self.cstruct.resolve(names[0])
268
-
269
- if tokens.next != self.TOK.BLOCK:
270
- raise ParserError(f"line {self._lineno(tokens.next)}: expected start of block '{tokens.next}'")
271
-
272
- fields = []
273
- tokens.consume()
274
- while len(tokens):
275
- if tokens.next == self.TOK.BLOCK and tokens.next.value == "}":
276
- tokens.consume()
277
- break
158
+ raise self._error(f"unexpected token {token.value!r}")
278
159
 
279
- if self._check_conditional(tokens):
280
- continue
160
+ if self._conditional_stack:
161
+ raise self._error("unclosed conditional statement", token=self._conditional_stack[-1][0])
281
162
 
282
- field = self._parse_field(tokens)
283
- fields.append(field)
163
+ # Preprocessor directives
284
164
 
285
- if register:
286
- names.extend(self._names(tokens))
165
+ def _parse_define(self) -> None:
166
+ """Parse a define directive and add the constant."""
167
+ self._expect(TokenType.PP_DEFINE)
287
168
 
288
- # If the next token is EOL, consume it
289
- # Otherwise we're part of a typedef or field definition
290
- if tokens.next == self.TOK.EOL:
291
- tokens.eol()
169
+ name_token = self._expect(TokenType.IDENTIFIER)
292
170
 
293
- name = names[0] if names else None
171
+ # If there's a value, it's emitted as a single raw STRING token
172
+ value = ""
173
+ if (token := self._current()).type == TokenType.STRING and token.line == name_token.line:
174
+ value = self._take().value
294
175
 
295
- if st is None:
296
- is_anonymous = False
297
- if not name:
298
- is_anonymous = True
299
- name = self.cstruct._next_anonymous()
176
+ if value:
177
+ if value[0] in ('"', "'"):
178
+ quote = value[0]
179
+ if value[-1] != quote:
180
+ raise self._error("unterminated string literal", token=token)
300
181
 
301
- st = factory(name, fields, align=self.align, anonymous=is_anonymous)
302
- if self.compiled and "nocompile" not in tokens.flags:
303
- st = compiler.compile(st)
304
- else:
305
- st.__fields__.extend(fields)
306
- st.commit()
307
-
308
- # This is pretty dirty
309
- if register:
310
- if not names and not registered:
311
- raise ParserError(f"line {self._lineno(stype)}: struct has no name")
312
-
313
- for name in names:
314
- self.cstruct.add_type(name, st)
315
-
316
- tokens.reset_flags()
317
- return st
318
-
319
- def _lookup(self, tokens: TokenConsumer) -> None:
320
- # Just like enums, we cheat and have the entire lookup in the token
321
- ltok = tokens.consume()
322
-
323
- pattern = self.TOK.patterns[self.TOK.LOOKUP]
324
- # Dirty trick because the regex expects a ; but we don't want it to be part of the value
325
- m = pattern.match(ltok.value + ";")
326
- d = ast.literal_eval(m.group(2))
327
- self.cstruct.lookups[m.group(1)] = {self.cstruct.consts[k]: v for k, v in d.items()}
328
-
329
- def _parse_field(self, tokens: TokenConsumer) -> Field:
330
- type_ = None
331
- if tokens.next == self.TOK.IDENTIFIER:
332
- type_ = self.cstruct.resolve(self._identifier(tokens))
333
- elif tokens.next == self.TOK.STRUCT:
334
- type_ = self._struct(tokens)
335
-
336
- if tokens.next != self.TOK.NAME:
337
- return Field(None, type_, None)
338
-
339
- if tokens.next != self.TOK.NAME:
340
- raise ParserError(f"line {self._lineno(tokens.next)}: expected name, got {tokens.next!r}")
341
- nametok = tokens.consume()
342
-
343
- type_, name, bits = self._parse_field_type(type_, nametok.value)
344
-
345
- tokens.eol()
346
- return Field(name.strip(), type_, bits)
347
-
348
- def _parse_field_type(self, type_: type[BaseType], name: str) -> tuple[type[BaseType], str, int | None]:
349
- pattern = self.TOK.patterns[self.TOK.NAME]
350
- # Dirty trick because the regex expects a ; but we don't want it to be part of the value
351
- d = pattern.match(name + ";").groupdict()
352
-
353
- name = d["name"]
354
- count_expression = d["count"]
355
-
356
- while name.startswith("*"):
357
- name = name[1:]
358
- type_ = self.cstruct._make_pointer(type_)
359
-
360
- if count_expression is not None:
361
- # Poor mans multi-dimensional array by abusing the eager regex match of count
362
- counts = count_expression.split("][") if "][" in count_expression else [count_expression]
363
-
364
- for count in reversed(counts):
365
- if count == "":
366
- count = None
367
- else:
368
- count = Expression(count)
369
- try:
370
- count = count.evaluate(self.cstruct)
371
- except Exception:
372
- pass
373
-
374
- if issubclass(type_, BaseArray) and count is None:
375
- raise ParserError("Depth required for multi-dimensional array")
376
-
377
- type_ = self.cstruct._make_array(type_, count)
378
-
379
- return type_, name.strip(), int(d["bits"]) if d["bits"] else None
380
-
381
- def _names(self, tokens: TokenConsumer) -> list[str]:
382
- names = []
383
- while True:
384
- if tokens.next == self.TOK.EOL:
385
- tokens.eol()
386
- break
182
+ # Remove the surrounding quotes and flatten escape sequences
183
+ value = value[1:-1].encode().decode("unicode_escape")
184
+ elif value[:2].lower() in ("b'", 'b"'):
185
+ quote = value[1]
186
+ if value[-1] != quote:
187
+ raise self._error("unterminated bytes literal", token=token)
387
188
 
388
- if tokens.next not in (self.TOK.NAME, self.TOK.DEFS, self.TOK.IDENTIFIER):
389
- break
189
+ # Remove the leading b and surrounding quotes and flatten escape sequences
190
+ value = value[2:-1].encode().decode("unicode_escape")
191
+ value = ast.literal_eval(f"b{value!r}")
192
+ else:
193
+ try:
194
+ # Lazy mode, try to evaluate as a Python literal first (for simple constants)
195
+ value = ast.literal_eval(value)
196
+ except (ValueError, SyntaxError):
197
+ # Try to evaluate it as an expression in the context of current constants
198
+ if isinstance(value, str):
199
+ try:
200
+ value = Expression(value).evaluate(self.cs)
201
+ except (LexerError, ExpressionParserError):
202
+ # If evaluation fails, just keep it as a string (e.g. for macro-like constants)
203
+ pass
204
+
205
+ self.cs.add_const(name_token.value, value)
206
+
207
+ def _parse_undef(self) -> None:
208
+ """Parse an undef directive and remove the constant."""
209
+ self._expect(TokenType.PP_UNDEF)
210
+
211
+ name_token = self._expect(TokenType.IDENTIFIER)
212
+ try:
213
+ self.cs.del_const(name_token.value)
214
+ except KeyError:
215
+ raise self._error(f"constant {name_token.value!r} not defined", token=name_token) from None
216
+
217
+ def _parse_include(self) -> None:
218
+ """Parse an include directive and add the included file to the includes list."""
219
+ self._expect(TokenType.PP_INCLUDE)
220
+ self.cs.includes.append(self._expect(TokenType.STRING).value)
221
+
222
+ def _parse_config_flags(self) -> None:
223
+ """Parse configuration flags from a directive like ``#[flag1, flag2, ...]``."""
224
+ self._flags.extend(flag.strip() for flag in self._expect(TokenType.PP_FLAGS).value.split(","))
225
+
226
+ def _handle_conditional(self) -> None:
227
+ """Handle conditional directives: ``#ifdef``, ``#ifndef``, ``#else``, ``#endif``."""
228
+ if (token := self._take()).type not in (
229
+ TokenType.PP_IFDEF,
230
+ TokenType.PP_IFNDEF,
231
+ TokenType.PP_ELSE,
232
+ TokenType.PP_ENDIF,
233
+ ):
234
+ raise self._error("expected conditional directive")
235
+
236
+ if token.type == TokenType.PP_IFDEF:
237
+ name = self._expect(TokenType.IDENTIFIER).value
238
+ if self._conditional_stack and not self._conditional_stack[-1][1]:
239
+ # Parent is false, so this child is always false
240
+ self._conditional_stack.append((token, False))
241
+ else:
242
+ self._conditional_stack.append((token, name in self.cs.consts))
243
+
244
+ elif token.type == TokenType.PP_IFNDEF:
245
+ name = self._expect(TokenType.IDENTIFIER).value
246
+ if self._conditional_stack and not self._conditional_stack[-1][1]:
247
+ self._conditional_stack.append((token, False))
248
+ else:
249
+ self._conditional_stack.append((token, name not in self.cs.consts))
250
+
251
+ elif token.type == TokenType.PP_ELSE:
252
+ if not self._conditional_stack:
253
+ raise self._error("#else without matching #ifdef/#ifndef", token=token)
254
+
255
+ # Only flip if parent is true (or there's no parent)
256
+ parent_active = len(self._conditional_stack) < 2 or self._conditional_stack[-2][1]
257
+ if parent_active:
258
+ self._conditional_stack[-1] = (self._conditional_stack[-1][0], not self._conditional_stack[-1][1])
259
+
260
+ elif token.type == TokenType.PP_ENDIF:
261
+ if not self._conditional_stack:
262
+ raise self._error("#endif without matching #ifdef/#ifndef", token=token)
263
+ self._conditional_stack.pop()
264
+
265
+ # If we ended up in a false branch, skip ahead to the matching #else/#endif
266
+ if self._in_false_branch():
267
+ self._skip_false_branch()
268
+
269
+ def _skip_false_branch(self) -> None:
270
+ """Skip all tokens in a false conditional branch until the matching ``#else`` or ``#endif``."""
271
+ depth = 0
272
+ while self._current().type != TokenType.EOF:
273
+ token_type = self._current().type
274
+
275
+ if token_type in (TokenType.PP_IFDEF, TokenType.PP_IFNDEF):
276
+ depth += 1
277
+ self._pos += 1
278
+ # Skip the identifier argument
279
+ if self._current().type != TokenType.EOF:
280
+ self._pos += 1
281
+ elif token_type == TokenType.PP_ENDIF:
282
+ if depth == 0:
283
+ return
284
+ depth -= 1
285
+ self._pos += 1
286
+ elif token_type == TokenType.PP_ELSE:
287
+ if depth == 0:
288
+ return
289
+ self._pos += 1
290
+ else:
291
+ self._pos += 1
390
292
 
391
- ntoken = tokens.consume()
392
- if ntoken in (self.TOK.NAME, self.TOK.IDENTIFIER):
393
- names.append(ntoken.value.strip())
394
- elif ntoken == self.TOK.DEFS:
395
- names.extend([name.strip() for name in ntoken.value.strip().split(",")])
396
-
397
- return names
398
-
399
- def _include(self, tokens: TokenConsumer) -> None:
400
- include = tokens.consume()
401
- pattern = self.TOK.patterns[self.TOK.INCLUDE]
402
- match = pattern.match(include.value).groupdict()
403
-
404
- self.cstruct.includes.append(match["name"].strip().strip("'\""))
405
-
406
- @staticmethod
407
- def _remove_comments(string: str) -> str:
408
- # https://stackoverflow.com/a/18381470
409
- pattern = r"(\".*?\"|\'.*?\')|(/\*.*?\*/|//[^\r\n]*$)"
410
- # first group captures quoted strings (double or single)
411
- # second group captures comments (//single-line or /* multi-line */)
412
- regex = re.compile(pattern, re.MULTILINE | re.DOTALL)
413
-
414
- def _replacer(match: re.Match) -> str:
415
- # if the 2nd group (capturing comments) is not None,
416
- # it means we have captured a non-quoted (real) comment string.
417
- if comment := match.group(2):
418
- return "\n" * comment.count("\n") # so we will return empty to remove the comment
419
- # otherwise, we will return the 1st group
420
- return match.group(1) # captured quoted-string
421
-
422
- return regex.sub(_replacer, string)
423
-
424
- @staticmethod
425
- def _lineno(tok: Token) -> int:
426
- """Quick and dirty line number calculator."""
427
- match = tok.match
428
- return match.string.count("\n", 0, match.start()) + 1
429
-
430
- def _config_flag(self, tokens: TokenConsumer) -> None:
431
- flag_token = tokens.consume()
432
- pattern = self.TOK.patterns[self.TOK.CONFIG_FLAG]
433
- tok_dict = pattern.match(flag_token.value).groupdict()
434
- tokens.flags.extend(tok_dict["values"].split(","))
293
+ # Type definitions
435
294
 
436
- def parse(self, data: str) -> None:
437
- scanner = re.Scanner(self.TOK.tokens)
438
- data = self._remove_comments(data)
439
- tokens, remaining = scanner.scan(data)
440
-
441
- if len(remaining):
442
- lineno = data.count("\n", 0, len(data) - len(remaining))
443
- raise ParserError(f"line {lineno}: invalid syntax in definition")
444
-
445
- tokens = TokenConsumer(tokens)
446
- while True:
447
- token = tokens.next
448
- if token is None:
295
+ def _parse_typedef(self) -> None:
296
+ """Parse a typedef definition."""
297
+ self._expect(TokenType.TYPEDEF)
298
+
299
+ base_type = self._parse_type_spec()
300
+
301
+ # Parse one or more typedef names with modifiers (pointers, arrays)
302
+ while self._at(TokenType.IDENTIFIER, TokenType.STAR):
303
+ type_, name, bits = self._parse_field_name(base_type)
304
+ if bits is not None:
305
+ raise self._error("typedefs cannot have bitfields")
306
+
307
+ # For convenience, we assign the typedef name to anonymous structs/unions
308
+ if issubclass(base_type, Structure) and base_type.__anonymous__:
309
+ base_type.__anonymous__ = False
310
+ base_type.__name__ = name
311
+ base_type.__qualname__ = name
312
+
313
+ self.cs.add_type(name, type_)
314
+
315
+ if not self._match(TokenType.COMMA):
449
316
  break
450
317
 
451
- if self._check_conditional(tokens):
452
- continue
318
+ self._match(TokenType.SEMICOLON)
453
319
 
454
- if token == self.TOK.CONFIG_FLAG:
455
- self._config_flag(tokens)
456
- elif token == self.TOK.DEFINE:
457
- self._constant(tokens)
458
- elif token == self.TOK.UNDEF:
459
- self._undef(tokens)
460
- elif token == self.TOK.TYPEDEF:
461
- self._typedef(tokens)
462
- elif token == self.TOK.STRUCT:
463
- self._struct(tokens, register=True)
464
- elif token == self.TOK.ENUM:
465
- self._enum(tokens)
466
- elif token == self.TOK.LOOKUP:
467
- self._lookup(tokens)
468
- elif token == self.TOK.INCLUDE:
469
- self._include(tokens)
470
- else:
471
- raise ParserError(f"line {self._lineno(token)}: unexpected token {token!r}")
320
+ def _parse_struct_or_union(self) -> type[Structure]:
321
+ """Parse a struct or union definition.
472
322
 
473
- if self._conditionals:
474
- raise ParserError(f"line {self._lineno(tokens.previous)}: unclosed conditional statement")
323
+ If ``register`` is ``True``, the struct will be registered with its name (which is required).
324
+ Otherwise, the struct will not be registered and can only be used as an inline type for fields.
325
+ """
326
+ start_token = self._expect(TokenType.STRUCT, TokenType.UNION)
475
327
 
328
+ is_union = start_token.type == TokenType.UNION
329
+ factory = self.cs._make_union if is_union else self.cs._make_struct
476
330
 
477
- class CStyleParser(Parser):
478
- """Definition parser for C-like structure syntax (legacy parser).
331
+ type = None
332
+ name = None
479
333
 
480
- Args:
481
- cs: An instance of cstruct
482
- compiled: Whether structs should be compiled or not.
483
- """
334
+ if not self._at(TokenType.LBRACE):
335
+ if not self._at(TokenType.IDENTIFIER):
336
+ raise self._error("expected struct name or '{'", token=start_token)
484
337
 
485
- def __init__(self, cs: cstruct, compiled: bool = True):
486
- self.compiled = compiled
487
- super().__init__(cs)
338
+ name = self._take().value
488
339
 
489
- def _constants(self, data: str) -> None:
490
- r = re.finditer(r"#define\s+(?P<name>[^\s]+)\s+(?P<value>[^\r\n]+)\s*\n", data)
491
- for t in r:
492
- d = t.groupdict()
493
- v = d["value"].rsplit("//")[0]
494
-
495
- try:
496
- v = ast.literal_eval(v)
497
- except (ValueError, SyntaxError):
498
- pass
499
-
500
- self.cstruct.consts[d["name"]] = v
501
-
502
- def _enums(self, data: str) -> None:
503
- r = re.finditer(
504
- r"(?P<enumtype>enum|flag)\s+(?P<name>[^\s:{]+)\s*(:\s*(?P<type>[^\s]+)\s*)?\{(?P<values>[^}]+)\}\s*;",
505
- data,
506
- )
507
- for t in r:
508
- d = t.groupdict()
509
- enumtype = d["enumtype"]
510
-
511
- nextval = 0
512
- if enumtype == "flag":
513
- nextval = 1
514
-
515
- values = {}
516
- for line in d["values"].split("\n"):
517
- line, _, _ = line.partition("//")
518
- for v in line.split(","):
519
- key, _, val = v.partition("=")
520
- key = key.strip()
521
- val = val.strip()
522
- if not key:
523
- continue
524
-
525
- val = nextval if not val else Expression(val).evaluate(self.cstruct)
526
-
527
- if enumtype == "flag":
528
- high_bit = val.bit_length() - 1
529
- nextval = 2 ** (high_bit + 1)
530
- else:
531
- nextval = val + 1
532
-
533
- values[key] = val
534
-
535
- if not d["type"]:
536
- d["type"] = "uint32"
537
-
538
- factory = self.cstruct._make_enum
539
- if enumtype == "flag":
540
- factory = self.cstruct._make_flag
541
-
542
- enum = factory(d["name"], self.cstruct.resolve(d["type"]), values)
543
- self.cstruct.add_type(enum.__name__, enum)
544
-
545
- def _structs(self, data: str) -> None:
546
- r = re.finditer(
547
- r"(#(?P<flags>(?:compile))\s+)?"
548
- r"((?P<typedef>typedef)\s+)?"
549
- r"(?P<type>[^\s]+)\s+"
550
- r"(?P<name>[^\s]+)?"
551
- r"(?P<fields>"
552
- r"\s*{[^}]+\}(?P<defs>\s+[^;\n]+)?"
553
- r")?\s*;",
554
- data,
555
- )
556
- for t in r:
557
- d = t.groupdict()
558
-
559
- if d["name"]:
560
- name = d["name"]
561
- elif d["defs"]:
562
- name = d["defs"].strip().split(",")[0].strip()
340
+ # struct name { ... }
341
+ if self._at(TokenType.LBRACE):
342
+ # Named struct/union, empty pre-register for self-referencing
343
+ type = factory(name, [], align=self.align)
344
+ if self.compiled and "nocompile" not in self._flags:
345
+ type = compiler.compile(type)
346
+ self.cs.add_type(name, type)
563
347
  else:
564
- raise ParserError("No name for struct")
565
-
566
- if d["type"] == "struct":
567
- data = self._parse_fields(d["fields"][1:-1].strip())
568
- st = self.cstruct._make_struct(name, data)
569
- if d["flags"] == "compile" or self.compiled:
570
- st = compiler.compile(st)
571
- elif d["typedef"] == "typedef":
572
- st = d["type"]
573
- else:
574
- continue
348
+ # struct typename ... (type reference)
349
+ return self.cs.resolve(name)
575
350
 
576
- if d["name"]:
577
- self.cstruct.add_type(d["name"], st)
351
+ # Parse body
352
+ self._expect(TokenType.LBRACE)
353
+ fields = self._parse_field_list()
354
+ self._expect(TokenType.RBRACE)
578
355
 
579
- if d["defs"]:
580
- for td in d["defs"].strip().split(","):
581
- td = td.strip()
582
- self.cstruct.add_type(td, st)
356
+ if type is None:
357
+ is_anonymous = name is None
358
+ name = name or self.cs._next_anonymous()
583
359
 
584
- def _parse_fields(self, data: str) -> None:
585
- fields = re.finditer(
586
- r"(?P<type>[^\s]+)\s+(?P<name>[^\s\[:]+)(:(?P<bits>\d+))?(\[(?P<count>[^;\n]*)\])?;",
587
- data,
588
- )
360
+ type = factory(name, fields, align=self.align, anonymous=is_anonymous)
361
+ if self.compiled and "nocompile" not in self._flags:
362
+ type = compiler.compile(type)
363
+ else:
364
+ type.__fields__.extend(fields)
365
+ type.commit()
589
366
 
590
- result = []
591
- for f in fields:
592
- d = f.groupdict()
593
- if d["type"].startswith("//"):
594
- continue
367
+ self._flags.clear()
368
+ return type
595
369
 
596
- type_ = self.cstruct.resolve(d["type"])
370
+ def _parse_enum_or_flag(self) -> type[Enum | Flag]:
371
+ """Parse an enum or flag definition."""
372
+ start_token = self._expect(TokenType.ENUM, TokenType.FLAG)
597
373
 
598
- d["name"] = d["name"].replace("(", "").replace(")", "")
374
+ is_flag = start_token.type == TokenType.FLAG
599
375
 
600
- # Maybe reimplement lazy type references later
601
- # _type = TypeReference(self, d['type'])
602
- if d["count"] is not None:
603
- if d["count"] == "":
604
- count = None
605
- else:
606
- count = Expression(d["count"])
607
- try:
608
- count = count.evaluate(self.cstruct)
609
- except Exception:
610
- pass
376
+ name = None
377
+ if self._at(TokenType.IDENTIFIER):
378
+ name = self._take().value
611
379
 
612
- type_ = self.cstruct._make_array(type_, count)
380
+ # Optional base type
381
+ base_type_str = "uint32"
382
+ if self._match(TokenType.COLON):
383
+ parts = []
384
+ while (token := self._match(TokenType.IDENTIFIER)) is not None:
385
+ parts.append(token.value)
386
+ base_type_str = " ".join(parts)
613
387
 
614
- if d["name"].startswith("*"):
615
- d["name"] = d["name"][1:]
616
- type_ = self.cstruct._make_pointer(type_)
388
+ self._expect(TokenType.LBRACE)
617
389
 
618
- field = Field(d["name"], type_, int(d["bits"]) if d["bits"] else None)
619
- result.append(field)
390
+ next_value = 1 if is_flag else 0
391
+ values: dict[str, int] = {}
620
392
 
621
- return result
393
+ while not self._at(TokenType.RBRACE):
394
+ if self._handle_preprocessor():
395
+ continue
396
+ self._assert_not_eof()
397
+
398
+ # For historical reasons, we allow enum/flag member names to start with a digit
399
+ # E.g. `32BIT`
400
+ member_name = ""
401
+ if token := self._match(TokenType.NUMBER):
402
+ member_name += token.value
403
+ if token := self._match(TokenType.IDENTIFIER):
404
+ member_name += token.value
405
+ else:
406
+ member_name = self._expect(TokenType.IDENTIFIER).value
622
407
 
623
- def _lookups(self, data: str, consts: dict[str, int]) -> None:
624
- r = re.finditer(r"\$(?P<name>[^\s]+) = ({[^}]+})\w*\n", data)
408
+ if self._match(TokenType.EQUALS):
409
+ expression = self._collect_until(TokenType.COMMA, TokenType.RBRACE)
410
+ value = Expression(expression).evaluate(self.cs, values)
411
+ else:
412
+ value = next_value
625
413
 
626
- for t in r:
627
- d = ast.literal_eval(t.group(2))
628
- self.cstruct.lookups[t.group(1)] = {self.cstruct.consts[k]: v for k, v in d.items()}
414
+ if is_flag:
415
+ high_bit = value.bit_length() - 1
416
+ next_value = 2 ** (high_bit + 1)
417
+ else:
418
+ next_value = value + 1
629
419
 
630
- def parse(self, data: str) -> None:
631
- self._constants(data)
632
- self._enums(data)
633
- self._structs(data)
634
- self._lookups(data, self.cstruct.consts)
420
+ values[member_name] = value
421
+ self._match(TokenType.COMMA) # optional trailing comma
635
422
 
423
+ self._expect(TokenType.RBRACE)
636
424
 
637
- class Token:
638
- __slots__ = ("match", "token", "value")
425
+ factory = self.cs._make_flag if is_flag else self.cs._make_enum
426
+ type_ = factory(name or "", self.cs.resolve(base_type_str), values)
639
427
 
640
- def __init__(self, token: str, value: str, match: re.Match):
641
- self.token = token
642
- self.value = value
643
- self.match = match
428
+ if name is not None:
429
+ # Register the enum/flag type if it has a name
430
+ # Anonymous enums/flags are handled in the top level parse loop
431
+ self.cs.add_type(type_.__name__, type_)
644
432
 
645
- def __eq__(self, other: object) -> bool:
646
- if isinstance(other, Token):
647
- other = other.token
433
+ return type_
648
434
 
649
- return self.token == other
435
+ # Field parsing
650
436
 
651
- def __ne__(self, other: object) -> bool:
652
- return not self == other
437
+ def _parse_field_list(self) -> list[Field]:
438
+ """Parse a list of fields inside a struct/union body until the closing brace."""
439
+ fields: list[Field] = []
653
440
 
654
- def __repr__(self) -> str:
655
- return f"<Token.{self.token} value={self.value!r}>"
441
+ while not self._at(TokenType.RBRACE):
442
+ if self._handle_preprocessor():
443
+ continue
444
+ self._assert_not_eof()
656
445
 
446
+ fields.append(self._parse_field())
657
447
 
658
- class TokenCollection:
659
- def __init__(self):
660
- self.tokens: list[Token] = []
661
- self.lookup: dict[str, str] = {}
662
- self.patterns: dict[str, re.Pattern] = {}
448
+ # Handle multiple fields declared in the same line, e.g., `int x, y, z;` or `struct { ... } a, b;`
449
+ while self._match(TokenType.COMMA):
450
+ type_, name, bits = self._parse_field_name(fields[-1].type)
451
+ fields.append(Field(name, type_, bits))
663
452
 
664
- def __getattr__(self, attr: str) -> str | Any:
665
- try:
666
- return self.lookup[attr]
667
- except AttributeError:
668
- pass
453
+ self._expect(TokenType.SEMICOLON)
669
454
 
670
- return object.__getattribute__(self, attr)
455
+ return fields
671
456
 
672
- def add(self, regex: str, name: str | None) -> None:
673
- if name is None:
674
- self.tokens.append((regex, None))
675
- else:
676
- self.lookup[name] = name
677
- self.patterns[name] = re.compile(regex)
678
- self.tokens.append((regex, lambda s, t: Token(name, t, s.match)))
457
+ def _parse_field(self) -> Field:
458
+ """Parse a single field declaration."""
459
+ # Regular field: `type name`
460
+ type_ = self._parse_type_spec()
679
461
 
462
+ # Handle the case where a semicolon follows immediately (e.g., anonymous struct/unions)
463
+ if self._at(TokenType.SEMICOLON):
464
+ return Field(None, type_, None)
680
465
 
681
- class TokenConsumer:
682
- def __init__(self, tokens: list[Token]):
683
- self.tokens = tokens
684
- self.flags = []
685
- self.previous = None
466
+ type_, name, bits = self._parse_field_name(type_)
467
+ return Field(name, type_, bits)
686
468
 
687
- def __contains__(self, token: Token) -> bool:
688
- return token in self.tokens
469
+ def _parse_field_name(self, base_type: type[BaseType]) -> tuple[type[BaseType], str, int | None]:
470
+ """Parses ``'*'* IDENTIFIER ('[' expr? ']')* (':' NUMBER)?``."""
471
+ type_ = base_type
689
472
 
690
- def __len__(self) -> int:
691
- return len(self.tokens)
473
+ # Pointer stars
474
+ while self._match(TokenType.STAR):
475
+ type_ = self.cs._make_pointer(type_)
692
476
 
693
- def __repr__(self) -> str:
694
- return f"<TokenConsumer next={self.next!r}>"
477
+ # Field name
478
+ name = self._expect(*IDENTIFIER_TYPES).value
695
479
 
696
- @property
697
- def next(self) -> Token:
698
- try:
699
- return self.tokens[0]
700
- except IndexError:
701
- return None
480
+ # Array dimensions
481
+ type_ = self._parse_array_dimensions(type_)
482
+
483
+ # Bitfield
484
+ bits = None
485
+ if self._match(TokenType.COLON):
486
+ bits = int(self._expect(TokenType.NUMBER).value, 0)
702
487
 
703
- def consume(self) -> Token:
704
- self.previous = self.tokens.pop(0)
705
- return self.previous
488
+ return type_, name.strip(), bits
706
489
 
707
- def reset_flags(self) -> None:
708
- self.flags = []
490
+ def _parse_array_dimensions(self, base_type: type[BaseType]) -> type[BaseType]:
491
+ """Parse array dimensions following a field name, e.g., ``field[10][20]``."""
492
+ dimensions: list[int | Expression] = []
493
+
494
+ while self._match(TokenType.LBRACKET):
495
+ if self._at(TokenType.RBRACKET):
496
+ dimensions.append(None)
497
+ else:
498
+ expression = self._collect_until(TokenType.RBRACKET)
499
+ count = Expression(expression)
500
+ try:
501
+ count = count.evaluate(self.cs)
502
+ except Exception:
503
+ pass
504
+ dimensions.append(count)
505
+ self._expect(TokenType.RBRACKET)
506
+
507
+ type_ = base_type
508
+ for count in reversed(dimensions):
509
+ if issubclass(type_, BaseArray) and count is None:
510
+ raise ParserError("Depth required for multi-dimensional array")
511
+ type_ = self.cs._make_array(type_, count)
512
+
513
+ return type_
514
+
515
+ # Type resolution
516
+
517
+ def _parse_type_spec(self) -> type[BaseType]:
518
+ """Parse a type specifier, handling multi-word types like ``unsigned long long``.
519
+
520
+ Uses lookahead to disambiguate type words from field names: if the next identifier is followed by a
521
+ field delimiter (any of ``;[:,}``) it is the field name, not part of the type — unless the current accumulated
522
+ parts don't form a valid type yet.
523
+ """
524
+ first = self._current()
525
+
526
+ # Handle struct/union/enum/flag inline definitions as type specifiers
527
+ if first.type in (TokenType.STRUCT, TokenType.UNION):
528
+ return self._parse_struct_or_union()
529
+
530
+ if first.type in (TokenType.ENUM, TokenType.FLAG):
531
+ return self._parse_enum_or_flag()
532
+
533
+ # Otherwise, accumulate identifiers for the type specifier until we hit a non-identifier or a field delimiter
534
+ parts = [self._expect(TokenType.IDENTIFIER).value]
535
+
536
+ while self._at(TokenType.IDENTIFIER):
537
+ next_after = self._peek(1)
538
+
539
+ if next_after.type in (
540
+ TokenType.SEMICOLON,
541
+ TokenType.LBRACKET,
542
+ TokenType.COLON,
543
+ TokenType.COMMA,
544
+ TokenType.RBRACE,
545
+ ):
546
+ # This identifier is followed by a field delimiter, it should be the field name,
547
+ # UNLESS the current parts don't form a valid type yet.
548
+ if " ".join(parts) in self.cs.types:
549
+ break
550
+
551
+ # Current parts don't resolve, consume and hope this completes the type name
552
+ # (will error on resolve if not).
553
+ parts.append(self._take().value)
554
+ elif next_after.type == TokenType.STAR:
555
+ # Field name starts with * (pointer). This identifier is the last type word, consume it and then stop.
556
+ parts.append(self._take().value)
557
+ break
558
+ elif next_after.type == TokenType.IDENTIFIER:
559
+ # More identifiers follow, consume this one as part of the type.
560
+ parts.append(self._take().value)
561
+ else:
562
+ break
709
563
 
710
- def eol(self) -> None:
711
- token = self.consume()
712
- if token.token != "EOL":
713
- raise ParserError(f"line {self._lineno(token)}: expected EOL")
564
+ return self.cs.resolve(" ".join(parts))