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/__init__.py +9 -0
- cseq/acceptance.py +162 -0
- cseq/api.py +328 -0
- cseq/binary.py +122 -0
- cseq/cache.py +420 -0
- cseq/cli.py +895 -0
- cseq/compile_db.py +98 -0
- cseq/config.py +138 -0
- cseq/cpu_rules.py +48 -0
- cseq/dependencies.py +202 -0
- cseq/docs.py +21 -0
- cseq/dwarf.py +115 -0
- cseq/event_store.py +378 -0
- cseq/explain.py +113 -0
- cseq/fast_parser.py +286 -0
- cseq/html.py +485 -0
- cseq/html_bundle.py +464 -0
- cseq/hybrid_parser.py +120 -0
- cseq/linker.py +117 -0
- cseq/marker.py +294 -0
- cseq/model.py +367 -0
- cseq/parser.py +797 -0
- cseq/parser_contract_cases.json +69 -0
- cseq/parser_dependency_lock.json +59 -0
- cseq/parser_environment.py +186 -0
- cseq/parser_migration.py +126 -0
- cseq/plugin.py +216 -0
- cseq/project.py +305 -0
- cseq/query.py +62 -0
- cseq/resources/README.md +171 -0
- cseq/resources/design.md +6197 -0
- cseq/resources/verification_report.html +26 -0
- cseq/runtime.py +177 -0
- cseq/runtime_address.py +79 -0
- cseq/runtime_cpu.py +87 -0
- cseq/scanner.py +54 -0
- cseq/sequence.py +281 -0
- cseq/server.py +104 -0
- cseq/source_index.py +227 -0
- cseq/static_store.py +280 -0
- cseq/trace_analysis.py +336 -0
- cseq/trace_diff.py +56 -0
- cseq/tree_sitter_parser.py +468 -0
- cseq/valueflow.py +179 -0
- cseq-0.0.1.dist-info/METADATA +181 -0
- cseq-0.0.1.dist-info/RECORD +49 -0
- cseq-0.0.1.dist-info/WHEEL +5 -0
- cseq-0.0.1.dist-info/entry_points.txt +2 -0
- cseq-0.0.1.dist-info/top_level.txt +1 -0
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
|
+
)
|