cseq 0.0.1__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.
cseq/fast_parser.py ADDED
@@ -0,0 +1,286 @@
1
+ from __future__ import annotations
2
+
3
+ from bisect import bisect_right
4
+ from dataclasses import dataclass
5
+ from functools import lru_cache
6
+ import re
7
+ import sys
8
+
9
+ from .model import (
10
+ CallSiteRecord,
11
+ CompactFunctionRecord,
12
+ CompactFunctionTable,
13
+ FileParseArtifact,
14
+ FunctionRecord,
15
+ ParseStatus,
16
+ SourceRange,
17
+ TranslationUnit,
18
+ )
19
+ from .parser import ParserBackend
20
+
21
+ _C_KEYWORDS = {
22
+ "if", "for", "while", "switch", "return", "sizeof", "_Alignof", "_Generic",
23
+ }
24
+
25
+ _FUNC_RE = re.compile(
26
+ r"(?P<header>(?:(?P<storage>static)\s+)?(?:inline\s+)?"
27
+ r"(?:const\s+)?(?:void|char|short|int|long|float|double|signed|unsigned|[A-Za-z_]\w*)"
28
+ r"(?:\s+|\s*\*\s*)(?P<name>[A-Za-z_]\w*)\s*\((?P<params>[^;{}()]*)\)\s*)\{",
29
+ re.MULTILINE,
30
+ )
31
+ _CALL_RE = re.compile(r"\b([A-Za-z_]\w*)\s*\(")
32
+
33
+ # Constructs whose semantics/provenance require the semantic backend. The fast
34
+ # path deliberately claims only a narrow, auditable subset.
35
+ _COMPLEX_PATTERNS: tuple[tuple[str, re.Pattern[str]], ...] = (
36
+ ("preprocessor-conditional", re.compile(r"(?m)^\s*#\s*(?:if|ifdef|ifndef|elif|else|endif)\b")),
37
+ ("macro-definition", re.compile(r"(?m)^\s*#\s*define\b")),
38
+ ("function-pointer", re.compile(r"\(\s*\*\s*[A-Za-z_]\w*\s*\)")),
39
+ ("member-access", re.compile(r"(?:->|\.[A-Za-z_]\w*)")),
40
+ ("aggregate-or-type", re.compile(r"\b(?:struct|union|enum|typedef)\b")),
41
+ ("switch", re.compile(r"\b(?:switch|case|default)\b")),
42
+ ("loop", re.compile(r"\b(?:for|while|do)\b")),
43
+ ("goto", re.compile(r"\b(?:goto|setjmp|longjmp)\b")),
44
+ ("extension", re.compile(r"\b(?:asm|__asm__|__attribute__|__declspec|typeof|__typeof__)\b")),
45
+ ("reserved-extension", re.compile(r"\b__[A-Za-z_]\w*\b")),
46
+ ("ternary", re.compile(r"\?[^:]+:")),
47
+ )
48
+
49
+
50
+ @dataclass(frozen=True, slots=True)
51
+ class FastEligibility:
52
+ eligible: bool
53
+ reasons: tuple[str, ...] = ()
54
+
55
+
56
+ @dataclass(frozen=True, slots=True)
57
+ class FastPrepared:
58
+ raw: str
59
+ code_only: str
60
+ spans: tuple[tuple[str, int, int, int, int, str | None, str], ...]
61
+ line_starts: tuple[int, ...]
62
+ eligibility: FastEligibility
63
+
64
+
65
+ class FastLexicalParser(ParserBackend):
66
+ """Conservative lexical parser for simple C translation units.
67
+
68
+ It is intentionally not a replacement C compiler. It only returns an
69
+ artifact when the TU is inside a small subset whose direct-call structure is
70
+ unambiguous. Everything else is refined by HybridParser's semantic backend.
71
+ """
72
+
73
+ name = "fast-lexical-v1"
74
+
75
+ def prepare(self, translation_unit: TranslationUnit) -> FastPrepared:
76
+ raw = translation_unit.source_text if translation_unit.source_text is not None else translation_unit.source.path.read_text(encoding="utf-8", errors="replace")
77
+ masked = _mask_comments_and_literals(raw)
78
+ # Preprocessor include/pragma lines do not participate in lexical C
79
+ # structure. Blank them while preserving byte/character offsets.
80
+ code_only = re.sub(r"(?m)^[ \t]*#.*$", lambda m: " " * len(m.group(0)), masked)
81
+ reasons: list[str] = []
82
+ if translation_unit.arguments:
83
+ # Compile flags can alter parsing/active code. Until the fast path
84
+ # has a real preprocessor, defer such TUs to clang.
85
+ reasons.append("compile-arguments")
86
+ for name, pattern in _COMPLEX_PATTERNS[:2]:
87
+ if pattern.search(masked):
88
+ reasons.append(name)
89
+ for name, pattern in _COMPLEX_PATTERNS[2:]:
90
+ if pattern.search(code_only):
91
+ reasons.append(name)
92
+ spans = tuple(_function_spans(code_only))
93
+ if not spans and re.search(r"\b[A-Za-z_]\w*\s*\([^;{}]*\)\s*\{", code_only):
94
+ reasons.append("unrecognized-function-header")
95
+ # Calls nested under control flow need region/guard provenance. Do not
96
+ # rescan every function body here: parse_prepared already scans bodies
97
+ # for calls. If that scan finds a real call in a function containing an
98
+ # `if`, it raises and HybridParser refines the TU with clang.
99
+ return FastPrepared(
100
+ raw=raw,
101
+ code_only=code_only,
102
+ spans=spans,
103
+ line_starts=tuple(_line_starts(raw)),
104
+ eligibility=FastEligibility(not reasons, tuple(dict.fromkeys(reasons))),
105
+ )
106
+
107
+ def parse(self, translation_unit: TranslationUnit) -> FileParseArtifact:
108
+ prepared = self.prepare(translation_unit)
109
+ if not prepared.eligibility.eligible:
110
+ raise ValueError("TU is not eligible for fast parser: " + ", ".join(prepared.eligibility.reasons))
111
+ return self.parse_prepared(translation_unit, prepared)
112
+
113
+ def parse_prepared(self, translation_unit: TranslationUnit, prepared: FastPrepared) -> FileParseArtifact:
114
+ raw = prepared.raw
115
+ masked = prepared.code_only
116
+ line_starts = prepared.line_starts
117
+ functions = CompactFunctionTable(translation_unit.source.project_relative_path)
118
+ callsites: list[CallSiteRecord] = []
119
+
120
+ line_cursor = 0
121
+ line_count = len(line_starts)
122
+
123
+ def line_col_monotonic(pos: int) -> tuple[int, int]:
124
+ nonlocal line_cursor
125
+ while line_cursor + 1 < line_count and line_starts[line_cursor + 1] <= pos:
126
+ line_cursor += 1
127
+ return line_cursor + 1, pos - line_starts[line_cursor] + 1
128
+
129
+ for name, body_start, body_end, header_start, name_start, storage, params_text in prepared.spans:
130
+ start_line, start_col = line_col_monotonic(name_start)
131
+ end_line, end_col = line_col_monotonic(body_end)
132
+ storage = sys.intern(storage) if storage else None
133
+ tq = raw[header_start:name_start].strip()
134
+ type_qualifier = sys.intern(tq) if tq else None
135
+ functions.append_fields(
136
+ name, start_line, start_col, end_line, end_col,
137
+ storage, type_qualifier, _parameter_names(params_text),
138
+ )
139
+
140
+ body = masked[body_start:body_end]
141
+ caller_id: str | None = None
142
+ has_if = bool(re.search(r"\bif\s*\(", body))
143
+ for match in _CALL_RE.finditer(body):
144
+ callee = match.group(1)
145
+ if callee in _C_KEYWORDS:
146
+ continue
147
+ # A real call in a function that contains control flow needs
148
+ # region provenance. Refine rather than approximating it.
149
+ if has_if:
150
+ raise ValueError("call-under-if requires semantic refinement")
151
+ absolute = body_start + match.start(1)
152
+ open_paren = body_start + match.end(0) - 1
153
+ call_end = _balanced_call_end(masked, open_paren, body_end)
154
+ if call_end is None:
155
+ continue
156
+ line, col = line_col_monotonic(absolute)
157
+ end_l, end_c = line_col_monotonic(call_end)
158
+ if caller_id is None:
159
+ caller_id = _function_id(
160
+ translation_unit, name, start_line, start_col, storage, type_qualifier
161
+ )
162
+ callsites.append(CallSiteRecord(
163
+ caller_id=caller_id,
164
+ caller_name=name,
165
+ callee_name=callee,
166
+ raw_text=raw[absolute:call_end + 1].strip(),
167
+ source_path=translation_unit.source.project_relative_path,
168
+ source_range=SourceRange(line, col, end_l, end_c),
169
+ ))
170
+
171
+ return FileParseArtifact(
172
+ translation_unit=translation_unit,
173
+ status=ParseStatus.OK,
174
+ functions=functions,
175
+ callsites=callsites,
176
+ parser_backend=self.name,
177
+ )
178
+
179
+
180
+ def _function_spans(text: str) -> list[tuple[str, int, int, int, int, str | None, str]]:
181
+ out: list[tuple[str, int, int, int, int, str | None, str]] = []
182
+ for m in _FUNC_RE.finditer(text):
183
+ brace = m.end() - 1
184
+ end = _matching_brace(text, brace)
185
+ if end is None:
186
+ continue
187
+ out.append((m.group("name"), brace + 1, end, m.start("header"), m.start("name"), m.group("storage"), m.group("params")))
188
+ return out
189
+
190
+
191
+ def _matching_brace(text: str, start: int) -> int | None:
192
+ depth = 0
193
+ for i in range(start, len(text)):
194
+ c = text[i]
195
+ if c == "{":
196
+ depth += 1
197
+ elif c == "}":
198
+ depth -= 1
199
+ if depth == 0:
200
+ return i
201
+ return None
202
+
203
+
204
+ def _balanced_call_end(text: str, open_paren: int, body_end: int) -> int | None:
205
+ depth = 0
206
+ limit = min(len(text), body_end + 1)
207
+ for i in range(open_paren, limit):
208
+ c = text[i]
209
+ if c == "(":
210
+ depth += 1
211
+ elif c == ")":
212
+ depth -= 1
213
+ if depth == 0:
214
+ return i
215
+ return None
216
+
217
+
218
+ def _line_starts(text: str) -> list[int]:
219
+ starts = [0]
220
+ starts.extend(i + 1 for i, c in enumerate(text) if c == "\n")
221
+ return starts
222
+
223
+
224
+ def _line_col(pos: int, line_starts: tuple[int, ...] | list[int]) -> tuple[int, int]:
225
+ idx = bisect_right(line_starts, pos) - 1
226
+ return idx + 1, pos - line_starts[idx] + 1
227
+
228
+
229
+ @lru_cache(maxsize=4096)
230
+ def _parameter_names(params_text: str) -> tuple[str, ...]:
231
+ text = params_text.strip()
232
+ if not text or text == "void":
233
+ return ()
234
+ names: list[str] = []
235
+ for part in text.split(","):
236
+ part = part.strip()
237
+ if not part or part == "...":
238
+ continue
239
+ ids = re.findall(r"[A-Za-z_]\w*", part)
240
+ if ids:
241
+ names.append(sys.intern(ids[-1]))
242
+ return tuple(names)
243
+
244
+
245
+ def _function_id(tu: TranslationUnit, name: str, line: int, col: int, storage: str | None, qual_type: str | None) -> str:
246
+ return f"{tu.source.project_relative_path}:{line}:{col}:{storage or 'external'}:{name}:{qual_type or '?'}"
247
+
248
+
249
+ def _mask_comments_and_literals(text: str) -> str:
250
+ out = list(text)
251
+ i = 0
252
+ n = len(out)
253
+ while i < n:
254
+ if i + 1 < n and text[i:i+2] == "//":
255
+ j = text.find("\n", i + 2)
256
+ j = n if j < 0 else j
257
+ for k in range(i, j):
258
+ out[k] = " "
259
+ i = j
260
+ continue
261
+ if i + 1 < n and text[i:i+2] == "/*":
262
+ j = text.find("*/", i + 2)
263
+ j = n - 2 if j < 0 else j
264
+ for k in range(i, min(n, j + 2)):
265
+ if out[k] != "\n":
266
+ out[k] = " "
267
+ i = min(n, j + 2)
268
+ continue
269
+ if text[i] in {'"', "'"}:
270
+ quote = text[i]
271
+ j = i + 1
272
+ while j < n:
273
+ if text[j] == "\\":
274
+ j += 2
275
+ continue
276
+ if text[j] == quote:
277
+ j += 1
278
+ break
279
+ j += 1
280
+ for k in range(i, min(n, j)):
281
+ if out[k] != "\n":
282
+ out[k] = " "
283
+ i = j
284
+ continue
285
+ i += 1
286
+ return "".join(out)