python-delphi-lsp 1.0.0__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.
- delphiast/__init__.py +133 -0
- delphiast/binary.py +251 -0
- delphiast/comment_builder.py +29 -0
- delphiast/consts.py +345 -0
- delphiast/grammar.py +460 -0
- delphiast/lark_builder.py +2674 -0
- delphiast/lark_tokens.py +237 -0
- delphiast/lsp_server.py +1553 -0
- delphiast/nodes.py +371 -0
- delphiast/parser.py +182 -0
- delphiast/preprocessor.py +837 -0
- delphiast/project_indexer.py +313 -0
- delphiast/semantic.py +375 -0
- delphiast/semantic_builder.py +1384 -0
- delphiast/source_reader.py +15 -0
- delphiast/workspace.py +53 -0
- delphiast/writer.py +73 -0
- python_delphi_lsp-1.0.0.dist-info/METADATA +280 -0
- python_delphi_lsp-1.0.0.dist-info/RECORD +23 -0
- python_delphi_lsp-1.0.0.dist-info/WHEEL +5 -0
- python_delphi_lsp-1.0.0.dist-info/entry_points.txt +2 -0
- python_delphi_lsp-1.0.0.dist-info/licenses/LICENSE +373 -0
- python_delphi_lsp-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,837 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Callable, Iterable, Optional
|
|
6
|
+
|
|
7
|
+
from .source_reader import read_source_text
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
IncludeLoader = Callable[[str, str], Optional[tuple[str, str]]]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class SourceMapEntry:
|
|
15
|
+
file_name: str
|
|
16
|
+
line: int
|
|
17
|
+
col_offset: int = 0
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class PreprocessorProblem:
|
|
22
|
+
kind: str
|
|
23
|
+
message: str
|
|
24
|
+
file_name: str
|
|
25
|
+
line: int
|
|
26
|
+
col: int
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class CommentInfo:
|
|
31
|
+
kind: str
|
|
32
|
+
text: str
|
|
33
|
+
file_name: str
|
|
34
|
+
line: int
|
|
35
|
+
col: int
|
|
36
|
+
end_line: int
|
|
37
|
+
end_col: int
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class PreprocessedSource:
|
|
42
|
+
text: str
|
|
43
|
+
source_map: list[SourceMapEntry]
|
|
44
|
+
defines: set[str]
|
|
45
|
+
scoped_enums: bool
|
|
46
|
+
problems: list[PreprocessorProblem]
|
|
47
|
+
comments: list[CommentInfo]
|
|
48
|
+
|
|
49
|
+
def map_position(self, line: int, col: int) -> tuple[str, int, int]:
|
|
50
|
+
if line < 1 or line > len(self.source_map):
|
|
51
|
+
return ('', 0, 0)
|
|
52
|
+
entry = self.source_map[line - 1]
|
|
53
|
+
return (entry.file_name, entry.line, col + entry.col_offset)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class PreprocessorOptions:
|
|
58
|
+
use_defines: bool = True
|
|
59
|
+
compiler_version: float = 36.0
|
|
60
|
+
rtl_version: float = 36.0
|
|
61
|
+
scoped_enums: bool = False
|
|
62
|
+
option_states: dict[str, bool] = field(default_factory=dict)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class _ConditionalFrame:
|
|
67
|
+
parent_active: bool
|
|
68
|
+
branch_taken: bool
|
|
69
|
+
current_active: bool
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class _FileContext:
|
|
74
|
+
text: str
|
|
75
|
+
file_name: str
|
|
76
|
+
index: int = 0
|
|
77
|
+
line: int = 1
|
|
78
|
+
col: int = 1
|
|
79
|
+
pending_output: str = ''
|
|
80
|
+
pending_line: int = 1
|
|
81
|
+
|
|
82
|
+
def peek(self, offset: int = 0) -> str:
|
|
83
|
+
idx = self.index + offset
|
|
84
|
+
if idx < 0 or idx >= len(self.text):
|
|
85
|
+
return ''
|
|
86
|
+
return self.text[idx]
|
|
87
|
+
|
|
88
|
+
def advance(self, count: int = 1) -> str:
|
|
89
|
+
if count <= 0:
|
|
90
|
+
return ''
|
|
91
|
+
end = min(len(self.text), self.index + count)
|
|
92
|
+
chunk = self.text[self.index:end]
|
|
93
|
+
for ch in chunk:
|
|
94
|
+
if ch == '\n':
|
|
95
|
+
self.line += 1
|
|
96
|
+
self.col = 1
|
|
97
|
+
else:
|
|
98
|
+
self.col += 1
|
|
99
|
+
self.index = end
|
|
100
|
+
return chunk
|
|
101
|
+
|
|
102
|
+
def eof(self) -> bool:
|
|
103
|
+
return self.index >= len(self.text)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class _OutputBuffer:
|
|
107
|
+
def __init__(self) -> None:
|
|
108
|
+
self._lines: list[str] = ['']
|
|
109
|
+
self._line_map: list[SourceMapEntry] = []
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def text(self) -> str:
|
|
113
|
+
return '\n'.join(self._lines)
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def line_map(self) -> list[SourceMapEntry]:
|
|
117
|
+
return self._line_map
|
|
118
|
+
|
|
119
|
+
def append_char(self, ch: str, file_name: str, line: int) -> None:
|
|
120
|
+
self._ensure_mapping(file_name, line)
|
|
121
|
+
if ch == '\n':
|
|
122
|
+
self._lines.append('')
|
|
123
|
+
else:
|
|
124
|
+
self._lines[-1] += ch
|
|
125
|
+
|
|
126
|
+
def _ensure_mapping(self, file_name: str, line: int) -> None:
|
|
127
|
+
if len(self._line_map) < len(self._lines):
|
|
128
|
+
self._line_map.append(SourceMapEntry(file_name=file_name, line=line, col_offset=0))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class Preprocessor:
|
|
132
|
+
def __init__(
|
|
133
|
+
self,
|
|
134
|
+
*,
|
|
135
|
+
defines: Iterable[str] = (),
|
|
136
|
+
include_paths: Iterable[str] = (),
|
|
137
|
+
include_loader: Optional[IncludeLoader] = None,
|
|
138
|
+
options: Optional[PreprocessorOptions] = None,
|
|
139
|
+
) -> None:
|
|
140
|
+
self.defines: set[str] = {self._normalize_define(d) for d in defines if d}
|
|
141
|
+
self.include_paths = [Path(p) for p in include_paths]
|
|
142
|
+
self.include_loader = include_loader or self._default_include_loader
|
|
143
|
+
self.options = options or PreprocessorOptions()
|
|
144
|
+
self._apply_default_compiler_defines()
|
|
145
|
+
self.scoped_enums = self.options.scoped_enums
|
|
146
|
+
self._option_values: dict[str, str] = {}
|
|
147
|
+
self._option_stack: list[tuple[dict[str, str], bool]] = []
|
|
148
|
+
self._problems: list[PreprocessorProblem] = []
|
|
149
|
+
self._comments: list[CommentInfo] = []
|
|
150
|
+
self._conditional_stack: list[_ConditionalFrame] = []
|
|
151
|
+
|
|
152
|
+
def process(self, text: str, file_name: str) -> PreprocessedSource:
|
|
153
|
+
self._problems = []
|
|
154
|
+
self._comments = []
|
|
155
|
+
self._conditional_stack = []
|
|
156
|
+
self._option_stack = []
|
|
157
|
+
self.scoped_enums = self.options.scoped_enums
|
|
158
|
+
self._option_values = {
|
|
159
|
+
self._normalize_option_name(name): 'ON' if bool(value) else 'OFF'
|
|
160
|
+
for name, value in self.options.option_states.items()
|
|
161
|
+
}
|
|
162
|
+
if 'SCOPEDENUMS' in self._option_values:
|
|
163
|
+
self.scoped_enums = self._option_values['SCOPEDENUMS'] == 'ON'
|
|
164
|
+
else:
|
|
165
|
+
self._set_option('SCOPEDENUMS', 'ON' if self.scoped_enums else 'OFF')
|
|
166
|
+
|
|
167
|
+
normalized = self._normalize_newlines(text)
|
|
168
|
+
contexts = [_FileContext(text=normalized, file_name=file_name)]
|
|
169
|
+
include_stack = [file_name]
|
|
170
|
+
output = _OutputBuffer()
|
|
171
|
+
|
|
172
|
+
while contexts:
|
|
173
|
+
ctx = contexts[-1]
|
|
174
|
+
|
|
175
|
+
if ctx.pending_output:
|
|
176
|
+
pending = ctx.pending_output
|
|
177
|
+
ctx.pending_output = ''
|
|
178
|
+
pending_line = ctx.pending_line
|
|
179
|
+
ctx.pending_line = ctx.line
|
|
180
|
+
self._emit_text(output, pending, ctx.file_name, pending_line, active=True)
|
|
181
|
+
continue
|
|
182
|
+
|
|
183
|
+
if ctx.eof():
|
|
184
|
+
contexts.pop()
|
|
185
|
+
if include_stack:
|
|
186
|
+
include_stack.pop()
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
ch = ctx.peek()
|
|
190
|
+
|
|
191
|
+
if ch == "'":
|
|
192
|
+
start_line = ctx.line
|
|
193
|
+
active = self._is_active()
|
|
194
|
+
literal = self._consume_string(ctx)
|
|
195
|
+
self._emit_text(output, literal, ctx.file_name, start_line, active=active)
|
|
196
|
+
continue
|
|
197
|
+
|
|
198
|
+
if ch == '/' and ctx.peek(1) == '/':
|
|
199
|
+
start_line = ctx.line
|
|
200
|
+
start_col = ctx.col
|
|
201
|
+
active = self._is_active()
|
|
202
|
+
raw, content = self._consume_line_comment(ctx)
|
|
203
|
+
self._emit_text(output, raw, ctx.file_name, start_line, active=active)
|
|
204
|
+
if active:
|
|
205
|
+
self._record_comment('slashes', content, ctx.file_name, start_line, start_col, ctx)
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
if ch == '{':
|
|
209
|
+
start_line = ctx.line
|
|
210
|
+
start_col = ctx.col
|
|
211
|
+
active = self._is_active()
|
|
212
|
+
if ctx.peek(1) == '$':
|
|
213
|
+
raw_text, content = self._consume_brace_directive(ctx)
|
|
214
|
+
self._handle_directive(
|
|
215
|
+
raw_text,
|
|
216
|
+
content,
|
|
217
|
+
ctx,
|
|
218
|
+
start_line,
|
|
219
|
+
contexts,
|
|
220
|
+
include_stack,
|
|
221
|
+
output,
|
|
222
|
+
)
|
|
223
|
+
else:
|
|
224
|
+
raw, content = self._consume_brace_comment(ctx)
|
|
225
|
+
self._emit_text(output, raw, ctx.file_name, start_line, active=active)
|
|
226
|
+
if active:
|
|
227
|
+
self._record_comment('borland', content, ctx.file_name, start_line, start_col, ctx)
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
if ch == '(' and ctx.peek(1) == '*':
|
|
231
|
+
start_line = ctx.line
|
|
232
|
+
start_col = ctx.col
|
|
233
|
+
active = self._is_active()
|
|
234
|
+
if ctx.peek(2) == '$':
|
|
235
|
+
raw_text, content = self._consume_paren_directive(ctx)
|
|
236
|
+
self._handle_directive(
|
|
237
|
+
raw_text,
|
|
238
|
+
content,
|
|
239
|
+
ctx,
|
|
240
|
+
start_line,
|
|
241
|
+
contexts,
|
|
242
|
+
include_stack,
|
|
243
|
+
output,
|
|
244
|
+
)
|
|
245
|
+
else:
|
|
246
|
+
raw, content = self._consume_paren_comment(ctx)
|
|
247
|
+
self._emit_text(output, raw, ctx.file_name, start_line, active=active)
|
|
248
|
+
if active:
|
|
249
|
+
self._record_comment('ansi', content, ctx.file_name, start_line, start_col, ctx)
|
|
250
|
+
continue
|
|
251
|
+
|
|
252
|
+
start_line = ctx.line
|
|
253
|
+
ch = ctx.advance(1)
|
|
254
|
+
self._emit_text(output, ch, ctx.file_name, start_line, active=self._is_active())
|
|
255
|
+
|
|
256
|
+
return PreprocessedSource(
|
|
257
|
+
text=output.text,
|
|
258
|
+
source_map=output.line_map,
|
|
259
|
+
defines=self.defines,
|
|
260
|
+
scoped_enums=self.scoped_enums,
|
|
261
|
+
problems=self._problems,
|
|
262
|
+
comments=self._comments,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
def _apply_default_compiler_defines(self) -> None:
|
|
266
|
+
if not self.options.use_defines:
|
|
267
|
+
return
|
|
268
|
+
if 'FPC' in self.defines:
|
|
269
|
+
return
|
|
270
|
+
if self.options.compiler_version >= 20.0:
|
|
271
|
+
self.defines.add('CONDITIONALEXPRESSIONS')
|
|
272
|
+
self.defines.add('UNICODE')
|
|
273
|
+
version_define = int(round(self.options.compiler_version * 10))
|
|
274
|
+
self.defines.add(f'VER{version_define}')
|
|
275
|
+
|
|
276
|
+
def _emit_text(
|
|
277
|
+
self,
|
|
278
|
+
output: _OutputBuffer,
|
|
279
|
+
text: str,
|
|
280
|
+
file_name: str,
|
|
281
|
+
line: int,
|
|
282
|
+
*,
|
|
283
|
+
active: bool,
|
|
284
|
+
) -> None:
|
|
285
|
+
current_line = line
|
|
286
|
+
for ch in text:
|
|
287
|
+
if ch == '\n':
|
|
288
|
+
output.append_char('\n', file_name, current_line)
|
|
289
|
+
current_line += 1
|
|
290
|
+
else:
|
|
291
|
+
if active:
|
|
292
|
+
output.append_char(ch, file_name, current_line)
|
|
293
|
+
else:
|
|
294
|
+
output.append_char(' ', file_name, current_line)
|
|
295
|
+
|
|
296
|
+
def _is_active(self) -> bool:
|
|
297
|
+
if not self._conditional_stack:
|
|
298
|
+
return True
|
|
299
|
+
return self._conditional_stack[-1].current_active
|
|
300
|
+
|
|
301
|
+
def _handle_directive(
|
|
302
|
+
self,
|
|
303
|
+
directive_raw: str,
|
|
304
|
+
directive_content: str,
|
|
305
|
+
ctx: _FileContext,
|
|
306
|
+
start_line: int,
|
|
307
|
+
contexts: list[_FileContext],
|
|
308
|
+
include_stack: list[str],
|
|
309
|
+
output: _OutputBuffer,
|
|
310
|
+
) -> None:
|
|
311
|
+
name, param = self._parse_directive(directive_content)
|
|
312
|
+
replacement = self._replace_with_spaces(directive_raw)
|
|
313
|
+
|
|
314
|
+
if not name:
|
|
315
|
+
self._emit_text(output, replacement, ctx.file_name, start_line, active=True)
|
|
316
|
+
return
|
|
317
|
+
|
|
318
|
+
if name in {'IFDEF', 'IFNDEF', 'IF', 'IFOPT'}:
|
|
319
|
+
condition = False
|
|
320
|
+
if self._is_active():
|
|
321
|
+
if name == 'IFDEF':
|
|
322
|
+
condition = self._is_defined(param)
|
|
323
|
+
elif name == 'IFNDEF':
|
|
324
|
+
condition = not self._is_defined(param)
|
|
325
|
+
elif name == 'IF':
|
|
326
|
+
condition = self._evaluate_conditional_expression(param)
|
|
327
|
+
elif name == 'IFOPT':
|
|
328
|
+
condition = self._evaluate_ifopt(param)
|
|
329
|
+
frame = _ConditionalFrame(
|
|
330
|
+
parent_active=self._is_active(),
|
|
331
|
+
branch_taken=condition,
|
|
332
|
+
current_active=self._is_active() and condition,
|
|
333
|
+
)
|
|
334
|
+
self._conditional_stack.append(frame)
|
|
335
|
+
self._emit_text(output, replacement, ctx.file_name, start_line, active=True)
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
if name in {'ELSEIF', 'ELSE', 'ENDIF', 'IFEND'}:
|
|
339
|
+
if not self._conditional_stack:
|
|
340
|
+
self._problems.append(
|
|
341
|
+
PreprocessorProblem(
|
|
342
|
+
kind='directive',
|
|
343
|
+
message=f'unexpected {name}',
|
|
344
|
+
file_name=ctx.file_name,
|
|
345
|
+
line=start_line,
|
|
346
|
+
col=ctx.col,
|
|
347
|
+
)
|
|
348
|
+
)
|
|
349
|
+
self._emit_text(output, replacement, ctx.file_name, start_line, active=True)
|
|
350
|
+
return
|
|
351
|
+
|
|
352
|
+
frame = self._conditional_stack[-1]
|
|
353
|
+
if name == 'ELSEIF':
|
|
354
|
+
if not frame.parent_active:
|
|
355
|
+
frame.current_active = False
|
|
356
|
+
elif frame.branch_taken:
|
|
357
|
+
frame.current_active = False
|
|
358
|
+
else:
|
|
359
|
+
condition = self._evaluate_conditional_expression(param)
|
|
360
|
+
frame.current_active = frame.parent_active and condition
|
|
361
|
+
if condition:
|
|
362
|
+
frame.branch_taken = True
|
|
363
|
+
elif name == 'ELSE':
|
|
364
|
+
if not frame.parent_active:
|
|
365
|
+
frame.current_active = False
|
|
366
|
+
else:
|
|
367
|
+
frame.current_active = frame.parent_active and not frame.branch_taken
|
|
368
|
+
frame.branch_taken = True
|
|
369
|
+
else:
|
|
370
|
+
self._conditional_stack.pop()
|
|
371
|
+
self._emit_text(output, replacement, ctx.file_name, start_line, active=True)
|
|
372
|
+
return
|
|
373
|
+
|
|
374
|
+
if name in {'DEFINE', 'UNDEF'}:
|
|
375
|
+
if self._is_active() and self.options.use_defines:
|
|
376
|
+
for define_name in self._parse_define_list(param):
|
|
377
|
+
if name == 'DEFINE':
|
|
378
|
+
self.defines.add(define_name)
|
|
379
|
+
else:
|
|
380
|
+
self.defines.discard(define_name)
|
|
381
|
+
self._emit_text(output, replacement, ctx.file_name, start_line, active=True)
|
|
382
|
+
return
|
|
383
|
+
|
|
384
|
+
if name in {'SCOPEDENUMS'}:
|
|
385
|
+
if self._is_active():
|
|
386
|
+
self.scoped_enums = self._parse_on_off(param)
|
|
387
|
+
self._set_option('SCOPEDENUMS', 'ON' if self.scoped_enums else 'OFF')
|
|
388
|
+
self._emit_text(output, replacement, ctx.file_name, start_line, active=True)
|
|
389
|
+
return
|
|
390
|
+
|
|
391
|
+
if name in {'PUSHOPT', 'POPOPT'}:
|
|
392
|
+
if self._is_active():
|
|
393
|
+
if name == 'PUSHOPT':
|
|
394
|
+
self._option_stack.append((dict(self._option_values), self.scoped_enums))
|
|
395
|
+
else:
|
|
396
|
+
if self._option_stack:
|
|
397
|
+
self._option_values, self.scoped_enums = self._option_stack.pop()
|
|
398
|
+
else:
|
|
399
|
+
self._problems.append(
|
|
400
|
+
PreprocessorProblem(
|
|
401
|
+
kind='directive',
|
|
402
|
+
message='POPOPT without matching PUSHOPT',
|
|
403
|
+
file_name=ctx.file_name,
|
|
404
|
+
line=start_line,
|
|
405
|
+
col=ctx.col,
|
|
406
|
+
)
|
|
407
|
+
)
|
|
408
|
+
self._emit_text(output, replacement, ctx.file_name, start_line, active=True)
|
|
409
|
+
return
|
|
410
|
+
|
|
411
|
+
if name == 'OPT':
|
|
412
|
+
if self._is_active():
|
|
413
|
+
self._apply_opt_directive(param)
|
|
414
|
+
self._emit_text(output, replacement, ctx.file_name, start_line, active=True)
|
|
415
|
+
return
|
|
416
|
+
|
|
417
|
+
if name in {'I', 'INCLUDE'}:
|
|
418
|
+
if self._is_active():
|
|
419
|
+
include_name = self._extract_include_name(param)
|
|
420
|
+
if include_name:
|
|
421
|
+
resolved = self.include_loader(ctx.file_name, include_name)
|
|
422
|
+
if resolved is None:
|
|
423
|
+
self._problems.append(
|
|
424
|
+
PreprocessorProblem(
|
|
425
|
+
kind='include',
|
|
426
|
+
message=f'include not found: {include_name}',
|
|
427
|
+
file_name=ctx.file_name,
|
|
428
|
+
line=start_line,
|
|
429
|
+
col=ctx.col,
|
|
430
|
+
)
|
|
431
|
+
)
|
|
432
|
+
else:
|
|
433
|
+
content, resolved_path = resolved
|
|
434
|
+
if resolved_path in include_stack:
|
|
435
|
+
self._problems.append(
|
|
436
|
+
PreprocessorProblem(
|
|
437
|
+
kind='include',
|
|
438
|
+
message=f'include cycle detected: {resolved_path}',
|
|
439
|
+
file_name=ctx.file_name,
|
|
440
|
+
line=start_line,
|
|
441
|
+
col=ctx.col,
|
|
442
|
+
)
|
|
443
|
+
)
|
|
444
|
+
else:
|
|
445
|
+
include_stack.append(resolved_path)
|
|
446
|
+
ctx.pending_output = replacement
|
|
447
|
+
ctx.pending_line = start_line
|
|
448
|
+
normalized = self._normalize_newlines(content)
|
|
449
|
+
if not normalized.endswith('\n'):
|
|
450
|
+
normalized += '\n'
|
|
451
|
+
contexts.append(_FileContext(text=normalized, file_name=resolved_path))
|
|
452
|
+
return
|
|
453
|
+
self._emit_text(output, replacement, ctx.file_name, start_line, active=True)
|
|
454
|
+
return
|
|
455
|
+
|
|
456
|
+
if self._is_active() and self._apply_named_option(name, param):
|
|
457
|
+
self._emit_text(output, replacement, ctx.file_name, start_line, active=True)
|
|
458
|
+
return
|
|
459
|
+
|
|
460
|
+
self._emit_text(output, replacement, ctx.file_name, start_line, active=True)
|
|
461
|
+
|
|
462
|
+
def _parse_directive(self, directive_text: str) -> tuple[str, str]:
|
|
463
|
+
text = directive_text.strip()
|
|
464
|
+
if not text:
|
|
465
|
+
return ('', '')
|
|
466
|
+
name = []
|
|
467
|
+
idx = 0
|
|
468
|
+
while idx < len(text) and (text[idx].isalpha() or text[idx] == '_'):
|
|
469
|
+
name.append(text[idx])
|
|
470
|
+
idx += 1
|
|
471
|
+
if not name:
|
|
472
|
+
return ('', '')
|
|
473
|
+
param = text[idx:].strip()
|
|
474
|
+
return (''.join(name).upper(), param)
|
|
475
|
+
|
|
476
|
+
def _parse_define_list(self, param: str) -> list[str]:
|
|
477
|
+
if not param:
|
|
478
|
+
return []
|
|
479
|
+
cleaned = param.replace(',', ' ')
|
|
480
|
+
names = [self._normalize_define(part) for part in cleaned.split() if part]
|
|
481
|
+
return [n for n in names if n]
|
|
482
|
+
|
|
483
|
+
def _evaluate_ifopt(self, param: str) -> bool:
|
|
484
|
+
if not param:
|
|
485
|
+
return False
|
|
486
|
+
token = param.strip().upper()
|
|
487
|
+
if not token:
|
|
488
|
+
return False
|
|
489
|
+
if token.endswith('+') and len(token) > 1:
|
|
490
|
+
option_name = token[:-1].strip()
|
|
491
|
+
return self._get_option(option_name) == 'ON'
|
|
492
|
+
if token.endswith('-') and len(token) > 1:
|
|
493
|
+
option_name = token[:-1].strip()
|
|
494
|
+
return self._get_option(option_name) == 'OFF'
|
|
495
|
+
if '=' in token:
|
|
496
|
+
name, raw_state = token.split('=', 1)
|
|
497
|
+
expected = self._normalize_option_state(raw_state)
|
|
498
|
+
if not name.strip() or expected is None:
|
|
499
|
+
return False
|
|
500
|
+
return self._get_option(name) == expected
|
|
501
|
+
parts = token.split()
|
|
502
|
+
if len(parts) == 2:
|
|
503
|
+
expected = self._normalize_option_state(parts[1])
|
|
504
|
+
if expected is None:
|
|
505
|
+
return False
|
|
506
|
+
return self._get_option(parts[0]) == expected
|
|
507
|
+
return False
|
|
508
|
+
|
|
509
|
+
def _evaluate_conditional_expression(self, param: str) -> bool:
|
|
510
|
+
text = param.strip().upper()
|
|
511
|
+
if text.startswith('COMPILERVERSION'):
|
|
512
|
+
return self._evaluate_version(text, 'COMPILERVERSION', self.options.compiler_version)
|
|
513
|
+
if text.startswith('RTLVERSION'):
|
|
514
|
+
return self._evaluate_version(text, 'RTLVERSION', self.options.rtl_version)
|
|
515
|
+
if text.startswith('DEFINED(') or text.startswith('NOT DEFINED('):
|
|
516
|
+
return self._evaluate_defined_chain(text)
|
|
517
|
+
return False
|
|
518
|
+
|
|
519
|
+
def _evaluate_version(self, text: str, label: str, value: float) -> bool:
|
|
520
|
+
rest = text[len(label):].strip()
|
|
521
|
+
if not rest:
|
|
522
|
+
return False
|
|
523
|
+
parts = rest.split()
|
|
524
|
+
if len(parts) < 2:
|
|
525
|
+
return False
|
|
526
|
+
oper = parts[0]
|
|
527
|
+
num = parts[1]
|
|
528
|
+
try:
|
|
529
|
+
right = float(num)
|
|
530
|
+
except ValueError:
|
|
531
|
+
return False
|
|
532
|
+
if oper == '=':
|
|
533
|
+
return value == right
|
|
534
|
+
if oper == '<>':
|
|
535
|
+
return value != right
|
|
536
|
+
if oper == '<':
|
|
537
|
+
return value < right
|
|
538
|
+
if oper == '<=':
|
|
539
|
+
return value <= right
|
|
540
|
+
if oper == '>':
|
|
541
|
+
return value > right
|
|
542
|
+
if oper == '>=':
|
|
543
|
+
return value >= right
|
|
544
|
+
return False
|
|
545
|
+
|
|
546
|
+
def _evaluate_defined_chain(self, text: str) -> bool:
|
|
547
|
+
remaining = text
|
|
548
|
+
result = True
|
|
549
|
+
evaluation = None
|
|
550
|
+
while remaining.startswith('DEFINED(') or remaining.startswith('NOT DEFINED('):
|
|
551
|
+
if remaining.startswith('DEFINED('):
|
|
552
|
+
define_name, remaining = self._consume_defined(remaining, 'DEFINED(')
|
|
553
|
+
cond = self._is_defined(define_name)
|
|
554
|
+
else:
|
|
555
|
+
define_name, remaining = self._consume_defined(remaining, 'NOT DEFINED(')
|
|
556
|
+
cond = not self._is_defined(define_name)
|
|
557
|
+
if evaluation is None:
|
|
558
|
+
result = cond
|
|
559
|
+
elif evaluation == 'AND':
|
|
560
|
+
result = result and cond
|
|
561
|
+
elif evaluation == 'OR':
|
|
562
|
+
result = result or cond
|
|
563
|
+
remaining = remaining.lstrip()
|
|
564
|
+
if remaining.startswith('AND '):
|
|
565
|
+
evaluation = 'AND'
|
|
566
|
+
remaining = remaining[4:]
|
|
567
|
+
elif remaining.startswith('OR '):
|
|
568
|
+
evaluation = 'OR'
|
|
569
|
+
remaining = remaining[3:]
|
|
570
|
+
return result
|
|
571
|
+
|
|
572
|
+
def _consume_defined(self, text: str, prefix: str) -> tuple[str, str]:
|
|
573
|
+
rest = text[len(prefix):]
|
|
574
|
+
end = rest.find(')')
|
|
575
|
+
if end == -1:
|
|
576
|
+
return ('', '')
|
|
577
|
+
name = rest[:end].strip()
|
|
578
|
+
remaining = rest[end + 1:]
|
|
579
|
+
return (name, remaining)
|
|
580
|
+
|
|
581
|
+
def _parse_on_off(self, param: str) -> bool:
|
|
582
|
+
token = param.strip().upper()
|
|
583
|
+
if token in {'ON', '1', 'TRUE', '+'}:
|
|
584
|
+
return True
|
|
585
|
+
if token in {'OFF', '0', 'FALSE', '-'}:
|
|
586
|
+
return False
|
|
587
|
+
return self.scoped_enums
|
|
588
|
+
|
|
589
|
+
def _is_defined(self, name: str) -> bool:
|
|
590
|
+
return self._normalize_define(name) in self.defines
|
|
591
|
+
|
|
592
|
+
def _normalize_define(self, name: str) -> str:
|
|
593
|
+
return name.strip().upper()
|
|
594
|
+
|
|
595
|
+
def _normalize_option_name(self, name: str) -> str:
|
|
596
|
+
return name.strip().upper()
|
|
597
|
+
|
|
598
|
+
def _normalize_option_state(self, raw_state: str) -> Optional[str]:
|
|
599
|
+
token = raw_state.strip().upper()
|
|
600
|
+
if token in {'+', 'ON', '1', 'TRUE'}:
|
|
601
|
+
return 'ON'
|
|
602
|
+
if token in {'-', 'OFF', '0', 'FALSE'}:
|
|
603
|
+
return 'OFF'
|
|
604
|
+
if token == 'AUTO':
|
|
605
|
+
return 'AUTO'
|
|
606
|
+
return None
|
|
607
|
+
|
|
608
|
+
def _set_option(self, option_name: str, state: str) -> None:
|
|
609
|
+
name = self._normalize_option_name(option_name)
|
|
610
|
+
normalized = self._normalize_option_state(state)
|
|
611
|
+
if not name or normalized is None:
|
|
612
|
+
return
|
|
613
|
+
self._option_values[name] = normalized
|
|
614
|
+
if name == 'SCOPEDENUMS':
|
|
615
|
+
self.scoped_enums = normalized == 'ON'
|
|
616
|
+
|
|
617
|
+
def _get_option(self, option_name: str) -> str:
|
|
618
|
+
return self._option_values.get(self._normalize_option_name(option_name), '')
|
|
619
|
+
|
|
620
|
+
def _apply_named_option(self, name: str, param: str) -> bool:
|
|
621
|
+
if not name:
|
|
622
|
+
return False
|
|
623
|
+
cleaned = param.strip()
|
|
624
|
+
if not cleaned:
|
|
625
|
+
return False
|
|
626
|
+
if cleaned.startswith('='):
|
|
627
|
+
cleaned = cleaned[1:].strip()
|
|
628
|
+
state = self._normalize_option_state(cleaned)
|
|
629
|
+
if state is not None:
|
|
630
|
+
self._set_option(name, state)
|
|
631
|
+
return True
|
|
632
|
+
if len(cleaned) >= 2 and cleaned[0].isalpha() and cleaned[1] in {'+', '-'} and len(cleaned) == 2:
|
|
633
|
+
# Handles forms like {$OPT R+} where R+ is passed as the param token.
|
|
634
|
+
self._set_option(cleaned[0], cleaned[1])
|
|
635
|
+
return True
|
|
636
|
+
return False
|
|
637
|
+
|
|
638
|
+
def _apply_opt_directive(self, param: str) -> None:
|
|
639
|
+
if not param:
|
|
640
|
+
return
|
|
641
|
+
parts = [part for part in param.replace(',', ' ').split() if part]
|
|
642
|
+
index = 0
|
|
643
|
+
while index < len(parts):
|
|
644
|
+
part = parts[index]
|
|
645
|
+
if len(part) >= 2 and part[-1] in {'+', '-'}:
|
|
646
|
+
self._set_option(part[:-1], part[-1])
|
|
647
|
+
index += 1
|
|
648
|
+
continue
|
|
649
|
+
if '=' in part:
|
|
650
|
+
name, raw_state = part.split('=', 1)
|
|
651
|
+
self._set_option(name, raw_state)
|
|
652
|
+
index += 1
|
|
653
|
+
continue
|
|
654
|
+
if index + 1 < len(parts):
|
|
655
|
+
candidate_state = self._normalize_option_state(parts[index + 1])
|
|
656
|
+
if candidate_state is not None:
|
|
657
|
+
self._set_option(part, candidate_state)
|
|
658
|
+
index += 2
|
|
659
|
+
continue
|
|
660
|
+
index += 1
|
|
661
|
+
|
|
662
|
+
def _consume_line_comment(self, ctx: _FileContext) -> tuple[str, str]:
|
|
663
|
+
start = ctx.index
|
|
664
|
+
while not ctx.eof() and ctx.peek() not in {'\n'}:
|
|
665
|
+
ctx.advance(1)
|
|
666
|
+
raw = ctx.text[start:ctx.index]
|
|
667
|
+
return raw, raw[2:] if raw.startswith('//') else raw
|
|
668
|
+
|
|
669
|
+
def _consume_brace_comment(self, ctx: _FileContext) -> tuple[str, str]:
|
|
670
|
+
start = ctx.index
|
|
671
|
+
ctx.advance(1)
|
|
672
|
+
while not ctx.eof():
|
|
673
|
+
if ctx.peek() == '}':
|
|
674
|
+
ctx.advance(1)
|
|
675
|
+
break
|
|
676
|
+
ctx.advance(1)
|
|
677
|
+
else:
|
|
678
|
+
self._problems.append(
|
|
679
|
+
PreprocessorProblem(
|
|
680
|
+
kind='comment',
|
|
681
|
+
message='unterminated comment',
|
|
682
|
+
file_name=ctx.file_name,
|
|
683
|
+
line=ctx.line,
|
|
684
|
+
col=ctx.col,
|
|
685
|
+
)
|
|
686
|
+
)
|
|
687
|
+
raw = ctx.text[start:ctx.index]
|
|
688
|
+
if raw.endswith('}'):
|
|
689
|
+
return raw, raw[1:-1]
|
|
690
|
+
return raw, raw[1:]
|
|
691
|
+
|
|
692
|
+
def _consume_paren_comment(self, ctx: _FileContext) -> tuple[str, str]:
|
|
693
|
+
start = ctx.index
|
|
694
|
+
ctx.advance(2)
|
|
695
|
+
while not ctx.eof():
|
|
696
|
+
if ctx.peek() == '*' and ctx.peek(1) == ')':
|
|
697
|
+
ctx.advance(2)
|
|
698
|
+
break
|
|
699
|
+
ctx.advance(1)
|
|
700
|
+
else:
|
|
701
|
+
self._problems.append(
|
|
702
|
+
PreprocessorProblem(
|
|
703
|
+
kind='comment',
|
|
704
|
+
message='unterminated comment',
|
|
705
|
+
file_name=ctx.file_name,
|
|
706
|
+
line=ctx.line,
|
|
707
|
+
col=ctx.col,
|
|
708
|
+
)
|
|
709
|
+
)
|
|
710
|
+
raw = ctx.text[start:ctx.index]
|
|
711
|
+
if raw.endswith('*)'):
|
|
712
|
+
return raw, raw[2:-2]
|
|
713
|
+
return raw, raw[2:]
|
|
714
|
+
|
|
715
|
+
def _consume_brace_directive(self, ctx: _FileContext) -> tuple[str, str]:
|
|
716
|
+
start = ctx.index
|
|
717
|
+
ctx.advance(2)
|
|
718
|
+
while not ctx.eof():
|
|
719
|
+
if ctx.peek() == '}':
|
|
720
|
+
ctx.advance(1)
|
|
721
|
+
break
|
|
722
|
+
ctx.advance(1)
|
|
723
|
+
else:
|
|
724
|
+
self._problems.append(
|
|
725
|
+
PreprocessorProblem(
|
|
726
|
+
kind='directive',
|
|
727
|
+
message='unterminated directive',
|
|
728
|
+
file_name=ctx.file_name,
|
|
729
|
+
line=ctx.line,
|
|
730
|
+
col=ctx.col,
|
|
731
|
+
)
|
|
732
|
+
)
|
|
733
|
+
token = ctx.text[start:ctx.index]
|
|
734
|
+
content = token[2:-1].strip() if token.endswith('}') else token[2:].strip()
|
|
735
|
+
return token, content
|
|
736
|
+
|
|
737
|
+
def _consume_paren_directive(self, ctx: _FileContext) -> tuple[str, str]:
|
|
738
|
+
start = ctx.index
|
|
739
|
+
ctx.advance(3)
|
|
740
|
+
while not ctx.eof():
|
|
741
|
+
if ctx.peek() == '*' and ctx.peek(1) == ')':
|
|
742
|
+
ctx.advance(2)
|
|
743
|
+
break
|
|
744
|
+
ctx.advance(1)
|
|
745
|
+
else:
|
|
746
|
+
self._problems.append(
|
|
747
|
+
PreprocessorProblem(
|
|
748
|
+
kind='directive',
|
|
749
|
+
message='unterminated directive',
|
|
750
|
+
file_name=ctx.file_name,
|
|
751
|
+
line=ctx.line,
|
|
752
|
+
col=ctx.col,
|
|
753
|
+
)
|
|
754
|
+
)
|
|
755
|
+
token = ctx.text[start:ctx.index]
|
|
756
|
+
if token.endswith('*)'):
|
|
757
|
+
content = token[3:-2].strip()
|
|
758
|
+
else:
|
|
759
|
+
content = token[3:].strip()
|
|
760
|
+
return token, content
|
|
761
|
+
|
|
762
|
+
def _consume_string(self, ctx: _FileContext) -> str:
|
|
763
|
+
start = ctx.index
|
|
764
|
+
ctx.advance(1)
|
|
765
|
+
while not ctx.eof():
|
|
766
|
+
ch = ctx.peek()
|
|
767
|
+
if ch == "'":
|
|
768
|
+
if ctx.peek(1) == "'":
|
|
769
|
+
ctx.advance(2)
|
|
770
|
+
continue
|
|
771
|
+
ctx.advance(1)
|
|
772
|
+
break
|
|
773
|
+
if ch == '\n':
|
|
774
|
+
self._problems.append(
|
|
775
|
+
PreprocessorProblem(
|
|
776
|
+
kind='string',
|
|
777
|
+
message='unterminated string literal',
|
|
778
|
+
file_name=ctx.file_name,
|
|
779
|
+
line=ctx.line,
|
|
780
|
+
col=ctx.col,
|
|
781
|
+
)
|
|
782
|
+
)
|
|
783
|
+
break
|
|
784
|
+
ctx.advance(1)
|
|
785
|
+
return ctx.text[start:ctx.index]
|
|
786
|
+
|
|
787
|
+
def _extract_include_name(self, param: str) -> str:
|
|
788
|
+
text = param.strip()
|
|
789
|
+
if not text:
|
|
790
|
+
return ''
|
|
791
|
+
if text[0] in {"'", '"'}:
|
|
792
|
+
quote = text[0]
|
|
793
|
+
end = text.find(quote, 1)
|
|
794
|
+
if end == -1:
|
|
795
|
+
return text[1:]
|
|
796
|
+
return text[1:end]
|
|
797
|
+
parts = text.split()
|
|
798
|
+
return parts[0] if parts else ''
|
|
799
|
+
|
|
800
|
+
def _replace_with_spaces(self, text: str) -> str:
|
|
801
|
+
return ''.join('\n' if ch == '\n' else ' ' for ch in text)
|
|
802
|
+
|
|
803
|
+
def _normalize_newlines(self, text: str) -> str:
|
|
804
|
+
return text.replace('\ufeff', '').replace('\r\n', '\n').replace('\r', '\n')
|
|
805
|
+
|
|
806
|
+
def _default_include_loader(self, parent_file: str, include_name: str) -> Optional[tuple[str, str]]:
|
|
807
|
+
parent_path = Path(parent_file)
|
|
808
|
+
include_path = Path(include_name.replace('\\', '/'))
|
|
809
|
+
search_paths = [parent_path.parent] + self.include_paths
|
|
810
|
+
for base in search_paths:
|
|
811
|
+
candidate = (base / include_path).resolve()
|
|
812
|
+
if candidate.exists():
|
|
813
|
+
return (read_source_text(candidate), str(candidate))
|
|
814
|
+
return None
|
|
815
|
+
|
|
816
|
+
def _record_comment(
|
|
817
|
+
self,
|
|
818
|
+
kind: str,
|
|
819
|
+
text: str,
|
|
820
|
+
file_name: str,
|
|
821
|
+
start_line: int,
|
|
822
|
+
start_col: int,
|
|
823
|
+
ctx: _FileContext,
|
|
824
|
+
) -> None:
|
|
825
|
+
end_line = ctx.line
|
|
826
|
+
end_col = max(ctx.col - 1, 1)
|
|
827
|
+
self._comments.append(
|
|
828
|
+
CommentInfo(
|
|
829
|
+
kind=kind,
|
|
830
|
+
text=text,
|
|
831
|
+
file_name=file_name,
|
|
832
|
+
line=start_line,
|
|
833
|
+
col=start_col,
|
|
834
|
+
end_line=end_line,
|
|
835
|
+
end_col=end_col,
|
|
836
|
+
)
|
|
837
|
+
)
|