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/html_bundle.py ADDED
@@ -0,0 +1,464 @@
1
+ from __future__ import annotations
2
+
3
+ from html import escape
4
+ import json
5
+ from pathlib import Path
6
+ import re
7
+ from typing import Iterable
8
+
9
+ from .html import render_html, build_html_payload
10
+ from .model import CpuEvidenceRecord
11
+ from .runtime import TraceSession
12
+ from .sequence import SequenceModel
13
+ from .trace_analysis import MarkerSuggestion
14
+ from .trace_diff import TraceDiffResult
15
+
16
+
17
+ def write_html_bundle(
18
+ output_html: str | Path,
19
+ model: SequenceModel,
20
+ *,
21
+ runtime_session: TraceSession | None = None,
22
+ cpu_evidence: Iterable[CpuEvidenceRecord] = (),
23
+ explanations: dict[str, list[dict[str, object]]] | None = None,
24
+ trace_diff: TraceDiffResult | None = None,
25
+ marker_suggestions: list[MarkerSuggestion] | None = None,
26
+ viewport_size: int = 200,
27
+ ) -> tuple[Path, Path]:
28
+ """Write a Large Bundle as ``index.html + .cseqdata``.
29
+
30
+ HTTP(S) opening uses fetch(). Local ``file://`` opening can be blocked by
31
+ browser policy, so the shell falls back to a file chooser for the same
32
+ sidecar. No external server or CDN is required.
33
+ """
34
+ html_path = Path(output_html)
35
+ data_path = html_path.with_suffix(".cseqdata")
36
+
37
+ payload = build_html_payload(
38
+ model,
39
+ runtime_session=runtime_session,
40
+ cpu_evidence=cpu_evidence,
41
+ explanations=explanations,
42
+ trace_diff=trace_diff,
43
+ marker_suggestions=marker_suggestions,
44
+ viewport_size=viewport_size,
45
+ )
46
+ data_path.write_text(
47
+ json.dumps(payload, ensure_ascii=False, sort_keys=True),
48
+ encoding="utf-8",
49
+ )
50
+
51
+ # Build only a tiny shell. The large runtime payload never passes through
52
+ # a Portable-HTML string in Bundle mode.
53
+ portable = render_html(model, title="cseq sequence", viewport_size=viewport_size)
54
+ match = re.search(
55
+ r'<script type="application/json" id="cseq-data">(.*?)</script>',
56
+ portable,
57
+ re.S,
58
+ )
59
+ if not match:
60
+ raise ValueError("embedded cseq data not found")
61
+ meta = json.dumps({"sidecar": data_path.name}, ensure_ascii=False)
62
+ shell = portable[: match.start()] + (
63
+ f'<script type="application/json" id="cseq-bundle-meta">{meta}</script>'
64
+ ) + portable[match.end() :]
65
+
66
+ loader = r'''async function loadCseqBundle(){
67
+ const meta=JSON.parse(document.getElementById('cseq-bundle-meta').textContent);
68
+ try {
69
+ const response=await fetch(meta.sidecar,{cache:'no-store'});
70
+ if(!response.ok) throw new Error(`HTTP ${response.status}`);
71
+ return await response.json();
72
+ } catch(fetchError) {
73
+ return await new Promise((resolve,reject)=>{
74
+ const wrap=document.createElement('div');
75
+ wrap.className='panel-body';
76
+ const msg=document.createElement('span');
77
+ msg.textContent=`Sidecar ${meta.sidecar} を選択してください(file:// fallback) `;
78
+ const input=document.createElement('input');
79
+ input.type='file';
80
+ input.accept='.cseqdata,application/json';
81
+ input.onchange=async()=>{
82
+ try {
83
+ const file=input.files?.[0];
84
+ if(!file) return;
85
+ const parsed=JSON.parse(await file.text());
86
+ wrap.remove();
87
+ resolve(parsed);
88
+ } catch(e) { reject(e); }
89
+ };
90
+ wrap.append(msg,input);
91
+ document.body.prepend(wrap);
92
+ });
93
+ }
94
+ }
95
+ '''
96
+ source_line = "const data = JSON.parse(document.getElementById('cseq-data').textContent);"
97
+ if source_line not in shell:
98
+ raise ValueError("portable bootstrap line not found")
99
+ shell = shell.replace(
100
+ source_line,
101
+ loader + "\nasync function cseqBoot(){\nconst data = await loadCseqBundle();",
102
+ 1,
103
+ )
104
+ close = "\n</script>\n</body>\n</html>"
105
+ if close not in shell:
106
+ raise ValueError("portable closing script not found")
107
+ shell = shell.replace(
108
+ close,
109
+ "\n}\ncseqBoot().catch(e=>{const pre=document.createElement('pre');pre.textContent='cseq bundle load failed: '+e;document.body.prepend(pre);});"
110
+ + close,
111
+ 1,
112
+ )
113
+ shell = shell.replace(
114
+ "</header>",
115
+ f'<div class="meta">bundle data: {escape(data_path.name)}</div></header>',
116
+ 1,
117
+ )
118
+ html_path.write_text(shell, encoding="utf-8")
119
+ return html_path, data_path
120
+
121
+
122
+ def write_html_bundle_from_event_store(
123
+ output_html: str | Path,
124
+ model: SequenceModel,
125
+ event_store,
126
+ session: str,
127
+ *,
128
+ cpu_evidence: Iterable[CpuEvidenceRecord] = (),
129
+ explanations: dict[str, list[dict[str, object]]] | None = None,
130
+ trace_diff: TraceDiffResult | None = None,
131
+ marker_suggestions: list[MarkerSuggestion] | None = None,
132
+ viewport_size: int = 200,
133
+ event_batch_size: int = 5000,
134
+ ) -> tuple[Path, Path]:
135
+ """Stream a Large Bundle directly from EventStore.
136
+
137
+ Runtime events are never materialized as a Python list. This is the
138
+ preferred path for million-event sessions already persisted in SQLite.
139
+ """
140
+ html_path = Path(output_html)
141
+ data_path = html_path.with_suffix('.cseqdata')
142
+ size = max(1, int(viewport_size))
143
+
144
+ payload = build_html_payload(
145
+ model,
146
+ runtime_session=None,
147
+ cpu_evidence=cpu_evidence,
148
+ explanations=explanations,
149
+ trace_diff=trace_diff,
150
+ marker_suggestions=marker_suggestions,
151
+ viewport_size=size,
152
+ )
153
+ payload.pop('runtime', None)
154
+ count = event_store.count(session)
155
+ payload['lod'] = 'summary' if count > 10000 else ('compact' if count > 1000 else 'detail')
156
+
157
+ tiles: list[dict[str, object]] = []
158
+ tile_events: list[dict[str, object]] = []
159
+ tile_start = 0
160
+
161
+ def event_row(e):
162
+ return {
163
+ 'event_index': e.event_index,
164
+ 'marker_id': e.marker_id,
165
+ 'timestamp': e.timestamp_raw,
166
+ 'cpu': e.cpu_hint,
167
+ 'task': e.task_hint,
168
+ 'clock_domain': e.clock_domain,
169
+ 'message': e.message,
170
+ 'payload': e.payload_dict(),
171
+ }
172
+
173
+ def flush_tile():
174
+ nonlocal tile_events, tile_start
175
+ if not tile_events:
176
+ return
177
+ tiles.append({
178
+ 'tile_index': len(tiles),
179
+ 'start_offset': tile_start,
180
+ 'end_offset': tile_start + len(tile_events) - 1,
181
+ 'start_event_index': tile_events[0]['event_index'],
182
+ 'end_event_index': tile_events[-1]['event_index'],
183
+ 'cpus': sorted({x['cpu'] for x in tile_events if x['cpu']}),
184
+ 'tasks': sorted({x['task'] for x in tile_events if x['task']}),
185
+ 'marker_count': len({x['marker_id'] for x in tile_events}),
186
+ })
187
+ tile_start += len(tile_events)
188
+ tile_events = []
189
+
190
+ # Write all non-runtime fields first, then stream the event array. JSON key
191
+ # order is not semantically significant.
192
+ with data_path.open('w', encoding='utf-8', newline='') as fh:
193
+ fh.write('{')
194
+ first_key = True
195
+ for key, value in payload.items():
196
+ if not first_key:
197
+ fh.write(',')
198
+ first_key = False
199
+ fh.write(json.dumps(str(key), ensure_ascii=False)); fh.write(':')
200
+ fh.write(json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(',', ':')))
201
+ if not first_key:
202
+ fh.write(',')
203
+ fh.write('"runtime":{"session":')
204
+ fh.write(json.dumps(session, ensure_ascii=False))
205
+ fh.write(',"events":[')
206
+ first_event = True
207
+ for event in event_store.iter_events(session, batch_size=event_batch_size):
208
+ row = event_row(event)
209
+ if not first_event:
210
+ fh.write(',')
211
+ first_event = False
212
+ fh.write(json.dumps(row, ensure_ascii=False, sort_keys=True, separators=(',', ':')))
213
+ tile_events.append(row)
214
+ if len(tile_events) >= size:
215
+ flush_tile()
216
+ flush_tile()
217
+ fh.write('],"tiles":')
218
+ fh.write(json.dumps(tiles, ensure_ascii=False, sort_keys=True, separators=(',', ':')))
219
+ fh.write('}}')
220
+
221
+ # Reuse the standard bundle shell, but without rebuilding the large data.
222
+ portable = render_html(model, title='cseq sequence', viewport_size=size)
223
+ match = re.search(r'<script type="application/json" id="cseq-data">(.*?)</script>', portable, re.S)
224
+ if not match:
225
+ raise ValueError('embedded cseq data not found')
226
+ meta = json.dumps({'sidecar': data_path.name}, ensure_ascii=False)
227
+ shell = portable[:match.start()] + f'<script type="application/json" id="cseq-bundle-meta">{meta}</script>' + portable[match.end():]
228
+ loader = r'''async function loadCseqBundle(){
229
+ const meta=JSON.parse(document.getElementById('cseq-bundle-meta').textContent);
230
+ try {
231
+ const response=await fetch(meta.sidecar,{cache:'no-store'});
232
+ if(!response.ok) throw new Error(`HTTP ${response.status}`);
233
+ return await response.json();
234
+ } catch(fetchError) {
235
+ return await new Promise((resolve,reject)=>{
236
+ const wrap=document.createElement('div'); wrap.className='panel-body';
237
+ const msg=document.createElement('span'); msg.textContent=`Sidecar ${meta.sidecar} を選択してください(file:// fallback) `;
238
+ const input=document.createElement('input'); input.type='file'; input.accept='.cseqdata,application/json';
239
+ input.onchange=async()=>{ try { const file=input.files?.[0]; if(!file)return; const parsed=JSON.parse(await file.text()); wrap.remove(); resolve(parsed); } catch(e){ reject(e); } };
240
+ wrap.append(msg,input); document.body.prepend(wrap);
241
+ });
242
+ }
243
+ }
244
+ '''
245
+ source_line = "const data = JSON.parse(document.getElementById('cseq-data').textContent);"
246
+ shell = shell.replace(source_line, loader + "\nasync function cseqBoot(){\nconst data = await loadCseqBundle();", 1)
247
+ close = "\n</script>\n</body>\n</html>"
248
+ shell = shell.replace(close, "\n}\ncseqBoot().catch(e=>{const pre=document.createElement('pre');pre.textContent='cseq bundle load failed: '+e;document.body.prepend(pre);});" + close, 1)
249
+ shell = shell.replace('</header>', f'<div class="meta">bundle data: {escape(data_path.name)}</div></header>', 1)
250
+ html_path.write_text(shell, encoding='utf-8')
251
+ return html_path, data_path
252
+
253
+
254
+ RANGE_INDEX_MAGIC = b"CSEQIDX2"
255
+ RANGE_INDEX_HEADER_SIZE = 32
256
+ RANGE_INDEX_ENTRY_SIZE = 16
257
+
258
+
259
+ def compact_range_index_size(event_count: int, viewport_size: int) -> int:
260
+ size = max(1, int(viewport_size))
261
+ count = max(0, int(event_count))
262
+ tiles = (count + size - 1) // size
263
+ return RANGE_INDEX_HEADER_SIZE + tiles * RANGE_INDEX_ENTRY_SIZE
264
+
265
+
266
+ def write_html_range_bundle_from_event_store(
267
+ output_html: str | Path,
268
+ model: SequenceModel,
269
+ event_store,
270
+ session: str,
271
+ *,
272
+ cpu_evidence: Iterable[CpuEvidenceRecord] = (),
273
+ explanations: dict[str, list[dict[str, object]]] | None = None,
274
+ trace_diff: TraceDiffResult | None = None,
275
+ marker_suggestions: list[MarkerSuggestion] | None = None,
276
+ viewport_size: int = 200,
277
+ ) -> tuple[Path, Path, Path]:
278
+ import struct
279
+
280
+ html_path = Path(output_html)
281
+ data_path = html_path.with_suffix('.cseqdata')
282
+ index_path = html_path.with_suffix('.cseqidx')
283
+ size = max(1, int(viewport_size))
284
+
285
+ def event_row(e):
286
+ return {
287
+ 'event_index': e.event_index,
288
+ 'marker_id': e.marker_id,
289
+ 'timestamp': e.timestamp_raw,
290
+ 'cpu': e.cpu_hint,
291
+ 'task': e.task_hint,
292
+ 'clock_domain': e.clock_domain,
293
+ 'message': e.message,
294
+ 'payload': e.payload_dict(),
295
+ }
296
+
297
+ entries: list[tuple[int, int]] = []
298
+ tile: list[dict[str, object]] = []
299
+ count = 0
300
+
301
+ def flush(fh):
302
+ nonlocal tile
303
+ if not tile:
304
+ return
305
+ start_pos = fh.tell()
306
+ raw = (json.dumps(tile, ensure_ascii=False, sort_keys=True, separators=(',', ':')) + '\n').encode('utf-8')
307
+ fh.write(raw)
308
+ entries.append((start_pos, fh.tell() - 1))
309
+ tile = []
310
+
311
+ with data_path.open('wb') as fh:
312
+ for event in event_store.iter_events(session, batch_size=max(1000, size * 4)):
313
+ tile.append(event_row(event))
314
+ count += 1
315
+ if len(tile) >= size:
316
+ flush(fh)
317
+ flush(fh)
318
+
319
+ with index_path.open('wb') as fh:
320
+ fh.write(struct.pack('<8sQQQ', RANGE_INDEX_MAGIC, count, size, len(entries)))
321
+ for byte_start, byte_end in entries:
322
+ fh.write(struct.pack('<QQ', byte_start, byte_end))
323
+
324
+ meta_payload = {
325
+ 'schema': 'cseq-range-bundle-v2',
326
+ 'session': session,
327
+ 'event_count': count,
328
+ 'viewport_size': size,
329
+ 'tile_count': len(entries),
330
+ 'lod': 'summary' if count > 10000 else ('compact' if count > 1000 else 'detail'),
331
+ 'data_file': data_path.name,
332
+ 'index_file': index_path.name,
333
+ 'index_header_size': RANGE_INDEX_HEADER_SIZE,
334
+ 'index_entry_size': RANGE_INDEX_ENTRY_SIZE,
335
+ 'cache_tiles': 8,
336
+ }
337
+
338
+ shell = render_html(
339
+ model,
340
+ title='cseq range bundle',
341
+ runtime_session=None,
342
+ cpu_evidence=cpu_evidence,
343
+ explanations=explanations,
344
+ trace_diff=trace_diff,
345
+ marker_suggestions=marker_suggestions,
346
+ viewport_size=size,
347
+ )
348
+ meta = json.dumps(meta_payload, ensure_ascii=False)
349
+ shell = shell.replace(
350
+ '</header>',
351
+ f'<div class="meta">range bundle v2: {escape(index_path.name)}</div></header>',
352
+ 1,
353
+ )
354
+ range_loader = r'''
355
+ <script type="application/json" id="cseq-range-meta">__META__</script>
356
+ <script>
357
+ (async()=>{
358
+ const timeline=document.getElementById('timeline');
359
+ const controls=document.getElementById('timeline-controls');
360
+ const meta=JSON.parse(document.getElementById('cseq-range-meta').textContent);
361
+ const tileCache=new Map();
362
+ function cacheGet(k){
363
+ if(!tileCache.has(k)) return null;
364
+ const v=tileCache.get(k); tileCache.delete(k); tileCache.set(k,v); return v;
365
+ }
366
+ function cachePut(k,v){
367
+ if(tileCache.has(k)) tileCache.delete(k);
368
+ tileCache.set(k,v);
369
+ while(tileCache.size>meta.cache_tiles) tileCache.delete(tileCache.keys().next().value);
370
+ }
371
+ async function fetchIndexEntry(tileNo){
372
+ const start=meta.index_header_size + tileNo*meta.index_entry_size;
373
+ const end=start + meta.index_entry_size - 1;
374
+ const r=await fetch(meta.index_file,{cache:'no-store',headers:{Range:`bytes=${start}-${end}`}});
375
+ if(!(r.status===206||r.status===200)) throw new Error(`index HTTP ${r.status}`);
376
+ const b=await r.arrayBuffer();
377
+ if(b.byteLength<meta.index_entry_size) throw new Error('short index entry');
378
+ const d=new DataView(b);
379
+ return {byte_start:Number(d.getBigUint64(0,true)),byte_end:Number(d.getBigUint64(8,true))};
380
+ }
381
+ async function fetchTile(tileNo){
382
+ const hit=cacheGet(tileNo); if(hit) return hit;
383
+ const entry=await fetchIndexEntry(tileNo);
384
+ const r=await fetch(meta.data_file,{cache:'no-store',headers:{Range:`bytes=${entry.byte_start}-${entry.byte_end}`}});
385
+ if(!(r.status===206||r.status===200)) throw new Error(`data HTTP ${r.status}`);
386
+ const events=JSON.parse((await r.text()).trim());
387
+ const startEvent=tileNo*meta.viewport_size;
388
+ const value={tile_index:tileNo,start_event_index:startEvent,end_event_index:Math.min(meta.event_count-1,startEvent+events.length-1),byte_start:entry.byte_start,byte_end:entry.byte_end,events};
389
+ cachePut(tileNo,value); return value;
390
+ }
391
+ function renderRows(value){
392
+ timeline.replaceChildren();
393
+ for(const e of value.events){
394
+ const row=document.createElement('div'); row.className='timeline-row';
395
+ for(const x of [e.event_index,e.timestamp||'?',e.cpu||'CPU?',e.task||'Task?',`${e.marker_id||''} ${e.message||''}`]){
396
+ const cell=document.createElement('div'); cell.textContent=String(x); row.appendChild(cell);
397
+ }
398
+ timeline.appendChild(row);
399
+ }
400
+ const m=document.createElement('div'); m.className='tile-summary';
401
+ m.textContent=`Range Tile ${value.tile_index+1}: events ${value.start_event_index}-${value.end_event_index} / bytes ${value.byte_start}-${value.byte_end}`;
402
+ timeline.prepend(m);
403
+ }
404
+ function prefetch(tileNo){
405
+ if(tileNo<0||tileNo>=meta.tile_count||tileCache.has(tileNo)) return;
406
+ fetchTile(tileNo).catch(()=>{});
407
+ }
408
+ async function loadTile(tileNo){
409
+ tileNo=Math.max(0,Math.min(meta.tile_count-1,Number(tileNo)||0));
410
+ const value=await fetchTile(tileNo); renderRows(value); input.value=String(tileNo+1);
411
+ prefetch(tileNo-1); prefetch(tileNo+1);
412
+ }
413
+ try{
414
+ const block=document.createElement('div'); block.className='path-controls';
415
+ const label=document.createElement('span'); label.textContent=`Range Bundle v2: ${meta.event_count} events / ${meta.tile_count} tiles`;
416
+ const input=document.createElement('input'); input.id='range-tile-input'; input.type='number'; input.min='1'; input.max=String(Math.max(1,meta.tile_count)); input.value='1'; input.style.width='8em';
417
+ const go=document.createElement('button'); go.textContent='移動';
418
+ const prev=document.createElement('button'); prev.textContent='前';
419
+ const next=document.createElement('button'); next.textContent='次';
420
+ block.append(label,prev,input,go,next); controls.replaceChildren(block);
421
+ go.onclick=()=>loadTile(Number(input.value)-1);
422
+ input.onkeydown=e=>{if(e.key==='Enter')loadTile(Number(input.value)-1);};
423
+ prev.onclick=()=>loadTile(Number(input.value)-2);
424
+ next.onclick=()=>loadTile(Number(input.value));
425
+ if(meta.tile_count) await loadTile(0);
426
+ }catch(e){
427
+ timeline.textContent=`Range Bundleを読み込めません。cseq serve等のHTTP配信で開いてください: ${e}`;
428
+ }
429
+ })();
430
+ </script>
431
+ '''.replace('__META__', meta.replace('</','<\\/'))
432
+ shell = shell.replace('</body>', range_loader + '\n</body>', 1)
433
+ html_path.write_text(shell, encoding='utf-8')
434
+ return html_path, data_path, index_path
435
+
436
+ def write_html_range_bundle(
437
+ output_html: str | Path,
438
+ model: SequenceModel,
439
+ *,
440
+ runtime_session: TraceSession,
441
+ cpu_evidence: Iterable[CpuEvidenceRecord] = (),
442
+ explanations: dict[str, list[dict[str, object]]] | None = None,
443
+ trace_diff: TraceDiffResult | None = None,
444
+ marker_suggestions: list[MarkerSuggestion] | None = None,
445
+ viewport_size: int = 200,
446
+ ) -> tuple[Path, Path, Path]:
447
+ """Range Bundle convenience wrapper for an in-memory TraceSession."""
448
+ import tempfile
449
+ from .event_store import EventStore
450
+ with tempfile.TemporaryDirectory(prefix='cseq-range-') as td:
451
+ db = Path(td) / 'events.db'
452
+ with EventStore(db) as store:
453
+ store.replace_session_events(runtime_session.name, iter(runtime_session.events), chunk_size=max(1000, viewport_size * 5), batch_size=max(1000, viewport_size * 5))
454
+ return write_html_range_bundle_from_event_store(
455
+ output_html,
456
+ model,
457
+ store,
458
+ runtime_session.name,
459
+ cpu_evidence=cpu_evidence,
460
+ explanations=explanations,
461
+ trace_diff=trace_diff,
462
+ marker_suggestions=marker_suggestions,
463
+ viewport_size=viewport_size,
464
+ )
cseq/hybrid_parser.py ADDED
@@ -0,0 +1,120 @@
1
+ from __future__ import annotations
2
+
3
+ from .fast_parser import FastLexicalParser
4
+ from .model import Diagnostic, FileParseArtifact, ParseStatus, SourceRange, TranslationUnit
5
+ from .parser import ClangJsonParser, ParserBackend
6
+ from .parser_environment import probe_parser_environment
7
+ from .tree_sitter_parser import TreeSitterCParser
8
+
9
+
10
+ class HybridParser(ParserBackend):
11
+ """Canonical Tree-sitter syntax with selective Clang semantic refinement.
12
+
13
+ When the locked Tree-sitter runtime is unavailable, the legacy lexical
14
+ parser remains a bounded fallback. Clang construction is lazy so syntax-only
15
+ work can still run on environments where semantic refinement is absent.
16
+ """
17
+
18
+ name = "hybrid-tree-sitter-clang-v3"
19
+
20
+ def __init__(self, fast: ParserBackend | None = None, semantic: ParserBackend | None = None) -> None:
21
+ self.fast = fast or self._default_fast()
22
+ self.semantic = semantic
23
+ self.fast_count = 0
24
+ self.semantic_count = 0
25
+ self.syntax_only_partial_count = 0
26
+
27
+ @staticmethod
28
+ def _default_fast() -> ParserBackend:
29
+ env = probe_parser_environment()
30
+ if env.canonical_fast_frontend_ready:
31
+ try:
32
+ return TreeSitterCParser()
33
+ except RuntimeError:
34
+ # Environment probing already exposes the mismatch. Keep the
35
+ # bounded legacy fallback rather than silently dropping parsing.
36
+ pass
37
+ return FastLexicalParser()
38
+
39
+ def _semantic_backend(self) -> ParserBackend | None:
40
+ if self.semantic is not None:
41
+ return self.semantic
42
+ try:
43
+ self.semantic = ClangJsonParser()
44
+ except RuntimeError:
45
+ return None
46
+ return self.semantic
47
+
48
+ def parse(self, translation_unit: TranslationUnit) -> FileParseArtifact:
49
+ prepared = self.fast.prepare(translation_unit) # type: ignore[attr-defined]
50
+ if prepared.eligibility.eligible:
51
+ try:
52
+ artifact = self.fast.parse_prepared(translation_unit, prepared) # type: ignore[attr-defined]
53
+ except Exception as exc:
54
+ semantic = self._semantic_backend()
55
+ if semantic is not None:
56
+ artifact = semantic.parse(translation_unit)
57
+ self.semantic_count += 1
58
+ return artifact
59
+ return _opaque_parser_failure(translation_unit, self.fast.name, exc)
60
+ self.fast_count += 1
61
+ return artifact
62
+
63
+ semantic = self._semantic_backend()
64
+ if semantic is not None:
65
+ self.semantic_count += 1
66
+ return semantic.parse(translation_unit)
67
+
68
+ # Tree-sitter syntax remains useful evidence even when Clang refinement
69
+ # is unavailable. Return it explicitly as PARTIAL rather than dropping
70
+ # the TU or pretending semantic completeness.
71
+ if isinstance(self.fast, TreeSitterCParser):
72
+ artifact = self.fast.parse_prepared(translation_unit, prepared)
73
+ artifact.status = ParseStatus.PARTIAL
74
+ artifact.diagnostics.append(
75
+ Diagnostic(
76
+ severity="warning",
77
+ message=(
78
+ "semantic refinement required but unavailable: "
79
+ + ", ".join(prepared.eligibility.reasons)
80
+ ),
81
+ )
82
+ )
83
+ artifact.parser_backend = "tree-sitter-c-v1+semantic-pending"
84
+ self.syntax_only_partial_count += 1
85
+ return artifact
86
+
87
+ # Legacy lexical parser cannot provide trustworthy syntax for an
88
+ # ineligible TU. Preserve the entire TU as explicit opaque evidence.
89
+ return FileParseArtifact(
90
+ translation_unit=translation_unit,
91
+ status=ParseStatus.OPAQUE_REGION,
92
+ diagnostics=[
93
+ Diagnostic(
94
+ severity="error",
95
+ message=(
96
+ "semantic backend unavailable for lexical-ineligible TU: "
97
+ + ", ".join(prepared.eligibility.reasons)
98
+ ),
99
+ )
100
+ ],
101
+ opaque_regions=[SourceRange()],
102
+ parser_backend="lexical-fallback+semantic-unavailable",
103
+ )
104
+
105
+
106
+ def _opaque_parser_failure(
107
+ translation_unit: TranslationUnit, backend_name: str, exc: Exception
108
+ ) -> FileParseArtifact:
109
+ return FileParseArtifact(
110
+ translation_unit=translation_unit,
111
+ status=ParseStatus.OPAQUE_REGION,
112
+ diagnostics=[
113
+ Diagnostic(
114
+ severity="error",
115
+ message=f"{backend_name} implementation failure with no semantic fallback: {type(exc).__name__}: {exc}",
116
+ )
117
+ ],
118
+ opaque_regions=[SourceRange()],
119
+ parser_backend=f"{backend_name}+failed",
120
+ )