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/project.py ADDED
@@ -0,0 +1,305 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from dataclasses import replace
5
+ import json
6
+ from concurrent.futures import ThreadPoolExecutor
7
+
8
+ from .binary import BinaryImage, apply_binary_evidence, load_binary_image
9
+ from .cache import (JsonCache, build_cache_key, deserialize_parse_artifact, serialize_parse_artifact,
10
+ build_analysis_cache_key, build_analysis_cache_key_from_fingerprints, serialize_analysis_snapshot, restore_analysis_snapshot, serialize_parse_artifact_light)
11
+ from .config import CseqConfig
12
+ from .compile_db import CompilationDatabase
13
+ from .cpu_rules import apply_cpu_rules
14
+ from .linker import LinkerMapIndex, apply_linker_map, parse_linker_map
15
+ from .dependencies import include_dependency_fingerprint
16
+ from .dwarf import DwarfLineIndex, apply_dwarf_evidence, load_dwarf_lines
17
+ from .model import ProjectIndex, TranslationUnit
18
+ from .parser import ParserBackend
19
+ from .hybrid_parser import HybridParser
20
+ from .plugin import PluginManager, PluginRunResult
21
+ from .scanner import scan_sources
22
+ from .source_index import SourceFingerprintIndex
23
+ from .sequence import resolve_direct_calls
24
+ from .static_store import StaticSymbolStore
25
+ from .valueflow import bind_direct_call_arguments, resolve_indirect_calls
26
+
27
+
28
+ class Project:
29
+ def __init__(
30
+ self,
31
+ root: str | Path,
32
+ *,
33
+ parser: ParserBackend | None = None,
34
+ config: CseqConfig | str | Path | None = None,
35
+ configuration: str | None = None,
36
+ defines: tuple[str, ...] = (),
37
+ cache_dir: str | Path | None = None,
38
+ use_cache: bool = True,
39
+ compile_db: str | Path | None = None,
40
+ linker_map: str | Path | None = None,
41
+ binary: str | Path | None = None,
42
+ dwarf: str | Path | None = None,
43
+ plugins: PluginManager | None = None,
44
+ jobs: int = 1,
45
+ static_store: str | Path | None = None,
46
+ ) -> None:
47
+ self.root = Path(root).resolve()
48
+ self.parser = parser or HybridParser()
49
+ if isinstance(config, (str, Path)):
50
+ self.config = CseqConfig.load(config)
51
+ else:
52
+ self.config = config or CseqConfig.empty()
53
+ self.configuration = configuration
54
+ self.defines = self.config.defines_for(configuration, defines)
55
+ self.use_cache = use_cache
56
+ parser_cache_root = Path(cache_dir) if cache_dir is not None else (self.root / ".cseq" / "cache" / "parser")
57
+ self.cache = JsonCache(parser_cache_root) if use_cache else None
58
+ # When functions are persisted in StaticSymbolStore, keep a lightweight
59
+ # artifact sidecar without the giant function array. This lets warm runs
60
+ # restore calls/value-flow/guards without json-loading all symbols again.
61
+ # Lightweight non-function parse artifacts are stored atomically in the
62
+ # Static Symbol Store TU table, so symbol/artifact reuse cannot drift.
63
+ self.static_artifact_hits = 0
64
+ self.static_artifact_misses = 0
65
+ analysis_root = (parser_cache_root.parent / "analysis") if parser_cache_root.name == "parser" else (parser_cache_root / "_analysis")
66
+ self.analysis_cache = JsonCache(analysis_root) if use_cache else None
67
+ self.cache_hits = 0
68
+ self.cache_misses = 0
69
+ self.analysis_cache_hits = 0
70
+ self.analysis_cache_misses = 0
71
+ db_path = Path(compile_db).resolve() if compile_db else (self.root / "compile_commands.json")
72
+ self.compile_db = CompilationDatabase.load(db_path) if db_path.is_file() else None
73
+ self.linker_map: LinkerMapIndex | None = parse_linker_map(linker_map) if linker_map is not None else None
74
+ self.binary_image: BinaryImage | None = load_binary_image(binary) if binary is not None else None
75
+ self.dwarf_index: DwarfLineIndex | None = load_dwarf_lines(dwarf) if dwarf is not None else None
76
+ self.plugins = plugins if plugins is not None else PluginManager.discover(self.config.plugins_enabled)
77
+ self.jobs = max(1, int(jobs))
78
+ self.plugin_result = PluginRunResult(diagnostics=list(self.plugins.diagnostics))
79
+ self.fast_artifacts = 0
80
+ self.semantic_artifacts = 0
81
+ resolved_static_store = static_store
82
+ if isinstance(static_store, str) and static_store == "auto":
83
+ resolved_static_store = self.root / ".cseq" / "static" / "symbols.db"
84
+ self.static_symbol_store = StaticSymbolStore(resolved_static_store) if resolved_static_store is not None else None
85
+ self.static_store_path = Path(resolved_static_store).resolve() if resolved_static_store is not None else None
86
+ source_index_enabled = self.cache is not None or self.static_symbol_store is not None
87
+ self.source_index_path = self.root / ".cseq" / "source" / "index.db"
88
+ self.source_index = SourceFingerprintIndex(self.source_index_path) if source_index_enabled else None
89
+ self.static_store_hits = 0
90
+ self.static_store_misses = 0
91
+ self.static_store_removed_tus = 0
92
+ if self.static_symbol_store is not None:
93
+ self.static_symbol_store.ensure_project(self.root)
94
+ self._closed = False
95
+
96
+ def close(self) -> None:
97
+ """Release persistent SQLite handles owned by this Project.
98
+
99
+ Windows does not permit deleting an open SQLite database, so resource
100
+ lifetime must be explicit rather than relying on POSIX unlink behavior.
101
+ Calling close more than once is safe.
102
+ """
103
+ if self._closed:
104
+ return
105
+ self._closed = True
106
+ if self.source_index is not None:
107
+ try:
108
+ self.source_index.close()
109
+ except Exception:
110
+ pass
111
+ if self.static_symbol_store is not None:
112
+ try:
113
+ self.static_symbol_store.close()
114
+ except Exception:
115
+ pass
116
+
117
+ def __enter__(self) -> "Project":
118
+ return self
119
+
120
+ def __exit__(self, exc_type, exc, tb) -> None:
121
+ self.close()
122
+
123
+ def __del__(self) -> None:
124
+ try:
125
+ self.close()
126
+ except Exception:
127
+ pass
128
+
129
+ def index(self) -> ProjectIndex:
130
+ files = scan_sources(self.root, fingerprint_index=self.source_index)
131
+ index = ProjectIndex(root=self.root, source_files=files, static_symbol_store=self.static_symbol_store)
132
+ config_define_args = tuple(f"-D{x}" for x in self.defines)
133
+ c_sources = [source for source in files if source.suffix == ".c"]
134
+ static_tu_state = (
135
+ self.static_symbol_store.translation_unit_state_raw()
136
+ if self.static_symbol_store is not None else {}
137
+ )
138
+
139
+ def parse_one(source):
140
+ compile_cmd = self.compile_db.lookup(source.path) if self.compile_db is not None else None
141
+ base_args = compile_cmd.arguments if compile_cmd is not None else ()
142
+ # Build the cheap TU identity first. Source text is opened only
143
+ # after persistent source/dependency and parser/static-store reuse
144
+ # checks miss. Warm unchanged TUs therefore do not open the .c body.
145
+ tu = TranslationUnit(
146
+ source=source,
147
+ arguments=tuple(base_args) + config_define_args,
148
+ working_directory=compile_cmd.directory if compile_cmd is not None else self.root,
149
+ configuration_name=self.configuration,
150
+ source_text=None,
151
+ )
152
+ # A TU fingerprint is required for safe Static Symbol Store reuse.
153
+ # Parser Cache already needs the same dependency walk, so compute it
154
+ # once and share it whenever either cache/store is enabled.
155
+ fingerprint = None
156
+ if self.cache is not None or self.static_symbol_store is not None:
157
+ dependency_hash = include_dependency_fingerprint(tu, self.source_index)
158
+ fingerprint = build_cache_key(
159
+ tu, parser_backend=self.parser.name, include_dependency_hash=dependency_hash
160
+ )
161
+ state = static_tu_state.get(tu.source.project_relative_path)
162
+ store_reusable = bool(
163
+ state is not None and fingerprint is not None and state[0] == fingerprint
164
+ )
165
+ if store_reusable:
166
+ try:
167
+ light = json.loads(state[1])
168
+ except (TypeError, json.JSONDecodeError):
169
+ light = None
170
+ if isinstance(light, dict) and light:
171
+ return deserialize_parse_artifact(
172
+ light, tu, include_functions=False
173
+ ), False, fingerprint, True, True
174
+ if self.cache is None:
175
+ try:
176
+ source_text = source.path.read_text(encoding="utf-8", errors="replace")
177
+ except OSError:
178
+ source_text = None
179
+ artifact = self.parser.parse(replace(tu, source_text=source_text))
180
+ return artifact, False, fingerprint, store_reusable, False
181
+ cached = self.cache.get(fingerprint)
182
+ if cached is not None:
183
+ return deserialize_parse_artifact(
184
+ cached, tu, include_functions=not store_reusable
185
+ ), True, fingerprint, store_reusable, False
186
+ try:
187
+ source_text = source.path.read_text(encoding="utf-8", errors="replace")
188
+ except OSError:
189
+ source_text = None
190
+ artifact = self.parser.parse(replace(tu, source_text=source_text))
191
+ payload = serialize_parse_artifact(artifact)
192
+ self.cache.put(fingerprint, payload)
193
+ return artifact, False, fingerprint, store_reusable, False
194
+
195
+ # Stream parse results into the ProjectIndex. In out-of-core mode,
196
+ # function metadata is written to SQLite immediately and released from
197
+ # each parse artifact before moving to the next TU. This prevents a
198
+ # temporary all-TU in-memory list from defeating the static store.
199
+ parser_fingerprints: list[str] = []
200
+ fast_count = 0
201
+ semantic_count = 0
202
+ static_dirty_tus = 0
203
+
204
+ def consume(result) -> None:
205
+ nonlocal fast_count, semantic_count, static_dirty_tus
206
+ artifact, cache_hit, parser_fingerprint, store_reusable, static_artifact_hit = result
207
+ if static_artifact_hit:
208
+ self.static_artifact_hits += 1
209
+ elif self.static_symbol_store is not None:
210
+ self.static_artifact_misses += 1
211
+ if parser_fingerprint is not None:
212
+ parser_fingerprints.append(parser_fingerprint)
213
+ if self.cache is not None:
214
+ if cache_hit:
215
+ self.cache_hits += 1
216
+ else:
217
+ self.cache_misses += 1
218
+ if artifact.parser_backend.startswith(("fast-lexical", "tree-sitter-c")):
219
+ fast_count += 1
220
+ elif artifact.parser_backend.startswith("clang-json"):
221
+ semantic_count += 1
222
+ else:
223
+ # Unknown parser strategies must not be silently classified as
224
+ # semantic evidence. Preserve conservative accounting.
225
+ semantic_count += 1
226
+ if self.static_symbol_store is not None:
227
+ source_path = artifact.translation_unit.source.project_relative_path
228
+ if store_reusable:
229
+ self.static_store_hits += 1
230
+ else:
231
+ self.static_store_misses += 1
232
+ if parser_fingerprint is None:
233
+ raise RuntimeError("static store requires a TU fingerprint")
234
+ self.static_symbol_store.replace_translation_unit(
235
+ source_path, parser_fingerprint, artifact.functions,
236
+ serialize_parse_artifact_light(artifact),
237
+ )
238
+ static_dirty_tus += 1
239
+ if static_dirty_tus >= 250:
240
+ # Keep cold out-of-core builds in bounded SQLite
241
+ # transactions. The expensive name index remains deferred
242
+ # until the final commit.
243
+ self.static_symbol_store.flush()
244
+ static_dirty_tus = 0
245
+ artifact.functions = []
246
+ # source_text is an ephemeral per-TU I/O optimization only. Do not
247
+ # retain project source bodies through parse_artifacts.
248
+ if artifact.translation_unit.source_text is not None:
249
+ artifact.translation_unit = replace(artifact.translation_unit, source_text=None)
250
+ index.parse_artifacts.append(artifact)
251
+ index.callsites.extend(artifact.callsites)
252
+ index.value_slots.extend(artifact.value_slots)
253
+ index.flow_edges.extend(artifact.flow_edges)
254
+ index.guarded_regions.extend(artifact.guarded_regions)
255
+
256
+ if self.jobs > 1 and len(c_sources) > 1:
257
+ with ThreadPoolExecutor(max_workers=self.jobs, thread_name_prefix="cseq-parse") as pool:
258
+ for result in pool.map(parse_one, c_sources):
259
+ consume(result)
260
+ else:
261
+ for source in c_sources:
262
+ consume(parse_one(source))
263
+
264
+ if self.static_symbol_store is not None:
265
+ current_c_sources = {s.project_relative_path for s in c_sources}
266
+ self.static_store_removed_tus = self.static_symbol_store.remove_missing_translation_units(
267
+ current_c_sources
268
+ )
269
+ self.static_symbol_store.commit()
270
+ analysis_key = None
271
+ restored = False
272
+ if self.analysis_cache is not None:
273
+ analysis_key = build_analysis_cache_key_from_fingerprints(parser_fingerprints, self.config, self.defines)
274
+ cached_analysis = self.analysis_cache.get(analysis_key)
275
+ if cached_analysis is not None:
276
+ restored = restore_analysis_snapshot(index, cached_analysis)
277
+ if restored:
278
+ self.analysis_cache_hits += 1
279
+ else:
280
+ if self.analysis_cache is not None:
281
+ self.analysis_cache_misses += 1
282
+ resolve_direct_calls(index)
283
+ bind_direct_call_arguments(index)
284
+ resolve_indirect_calls(index)
285
+ apply_cpu_rules(index, self.config, self.defines)
286
+ if self.analysis_cache is not None and analysis_key is not None:
287
+ self.analysis_cache.put(analysis_key, serialize_analysis_snapshot(index))
288
+ if self.linker_map is not None:
289
+ apply_linker_map(index, self.linker_map)
290
+ # Linker evidence can turn an ambiguous direct call into an exact one;
291
+ # bind its arguments now without mutating the cached source-only analysis.
292
+ bind_direct_call_arguments(index)
293
+ if self.binary_image is not None:
294
+ apply_binary_evidence(index, self.binary_image)
295
+ if self.dwarf_index is not None:
296
+ apply_dwarf_evidence(index, self.dwarf_index)
297
+ if self.plugins.plugins:
298
+ self.plugin_result = self.plugins.run_semantic_hints(functions=index.functions(), callsites=index.callsites)
299
+ else:
300
+ self.plugin_result = PluginRunResult(diagnostics=list(self.plugins.diagnostics))
301
+ self.fast_artifacts = fast_count
302
+ self.semantic_artifacts = semantic_count
303
+ if self.source_index is not None:
304
+ self.source_index.commit()
305
+ return index
cseq/query.py ADDED
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ from .model import CallTargetKind, ProjectIndex
4
+ from .trace_analysis import reconstruct_static_paths
5
+
6
+
7
+ def query_calls(index: ProjectIndex, function_name: str) -> dict:
8
+ incoming = []
9
+ outgoing = []
10
+ functions = {f.qualified_id: f for f in index.functions()}
11
+ for c in index.callsites:
12
+ target_names = []
13
+ tids = set(c.target_function_ids)
14
+ if c.target_function_id:
15
+ tids.add(c.target_function_id)
16
+ for tid in tids:
17
+ f = functions.get(tid)
18
+ if f:
19
+ target_names.append(f.name)
20
+ if c.caller_name == function_name:
21
+ outgoing.append({
22
+ "raw": c.raw_text,
23
+ "resolution": c.target_kind.value,
24
+ "targets": sorted(set(target_names)),
25
+ "line": c.source_range.start_line,
26
+ "source": c.source_path,
27
+ })
28
+ if function_name in target_names:
29
+ incoming.append({
30
+ "caller": c.caller_name,
31
+ "raw": c.raw_text,
32
+ "resolution": c.target_kind.value,
33
+ "line": c.source_range.start_line,
34
+ "source": c.source_path,
35
+ })
36
+ return {"function": function_name, "incoming": incoming, "outgoing": outgoing}
37
+
38
+
39
+ def query_unresolved(index: ProjectIndex) -> list[dict]:
40
+ unresolved = []
41
+ for c in index.callsites:
42
+ if c.target_kind in {CallTargetKind.UNKNOWN, CallTargetKind.INDIRECT_POSSIBLE, CallTargetKind.INTERNAL_POSSIBLE} or c.unknown_possible:
43
+ unresolved.append({
44
+ "caller": c.caller_name,
45
+ "raw": c.raw_text,
46
+ "resolution": c.target_kind.value,
47
+ "unknown_possible": c.unknown_possible,
48
+ "candidate_count": len(c.target_function_ids),
49
+ "source": c.source_path,
50
+ "line": c.source_range.start_line,
51
+ })
52
+ return unresolved
53
+
54
+
55
+ def query_paths(index: ProjectIndex, start: str, end: str) -> dict:
56
+ result = reconstruct_static_paths(index, start, end)
57
+ return {
58
+ "start": start,
59
+ "end": end,
60
+ "state": result.state.value,
61
+ "paths": [list(p) for p in result.paths],
62
+ }
@@ -0,0 +1,171 @@
1
+ # cseq
2
+
3
+ Cソースツリーから関数呼出し・分岐・コールバック・実行時markerを解析し、PlantUMLとインタラクティブHTMLを生成するシーケンス解析ツールです。
4
+
5
+ 初回リリースは **Windows + Python 3.11以上** を主対象とします。Tree-sitterを高速な構文フロントエンドとして使用し、型・macro・関数ポインタ等で意味解析が必要な翻訳単位だけClangへ送ります。
6
+
7
+ ## 主な機能
8
+
9
+ - ディレクトリ以下の `.c` / `.h` を再帰走査
10
+ - entry関数からのシーケンス生成
11
+ - 同名`static`関数を区別
12
+ - 外部未定義関数を内部calleeとして誤認しない
13
+ - `#if/#ifdef`、macro、compiler extensionのfail-soft解析
14
+ - 関数ポインタ、callback、dispatch tableの候補解決
15
+ - if/else、loop、無限loop、switch/case/defaultをSequence IRへ保持
16
+ - RTOS風の無限loopを有限表現
17
+ - CPU group / build configurationを外部設定可能
18
+ - runtime markerログの読込み、CPU/task evidence、静的経路との照合
19
+ - marker自動注入: source直接書換え / Overlay(原本非変更)の両方式
20
+ - PlantUML出力
21
+ - HTML UI: 検索、viewport、LOD、Canvas表示、path query、trace diff、marker suggestion
22
+ - 大きいruntime trace向けbundle/range出力
23
+ - optional linker map / binary symbol / DWARF evidence
24
+
25
+ ## Windowsでの導入
26
+
27
+ PowerShell例です。
28
+
29
+ ```powershell
30
+ py -3.12 -m venv .venv
31
+ .\.venv\Scripts\Activate.ps1
32
+ python -m pip install --upgrade pip
33
+ pip install "cseq[parser-tree-sitter]"
34
+ ```
35
+
36
+ ローカルwheelから導入する場合:
37
+
38
+ ```powershell
39
+ pip install .\cseq-0.0.1-py3-none-any.whl[parser-tree-sitter]
40
+ ```
41
+
42
+ ### Clang
43
+
44
+ 単純なC構文だけならTree-sitterで解析できますが、macro、関数ポインタ、型曖昧性、compiler extension等のselective semantic refinementにはClangを推奨します。
45
+
46
+ ```powershell
47
+ winget install --id LLVM.LLVM --exact
48
+ ```
49
+
50
+ 確認:
51
+
52
+ ```powershell
53
+ cseq doctor
54
+ ```
55
+
56
+ `current_execution_mode` が `tree-sitter+clang` なら両backendを利用できます。
57
+
58
+ ## 最短利用例
59
+
60
+ ```powershell
61
+ cseq analyze C:\path\to\project --entry main --json
62
+ cseq plantuml C:\path\to\project --entry main --output sequence.puml
63
+ cseq html C:\path\to\project --entry main --output sequence.html --mode auto
64
+ ```
65
+
66
+ HTMLは小規模時はportable単一HTML、中規模以上ではbundle/rangeへ切替できます。
67
+
68
+ ```powershell
69
+ cseq serve . --port 8000
70
+ ```
71
+
72
+ range/bundleをブラウザで確認する際はローカルHTTP配信を推奨します。
73
+
74
+ ## プロジェクト設定
75
+
76
+ ```powershell
77
+ cseq config init C:\path\to\project
78
+ cseq config validate C:\path\to\project\cseq.toml
79
+ ```
80
+
81
+ `cseq.toml`でentry、compile database、CPU group、build configuration等をプロジェクト固有名に合わせて設定します。cseq本体は特定企業・CPU名称を前提にしません。
82
+
83
+ `compile_commands.json` がある場合はClang semantic解析の精度向上に利用できます。
84
+
85
+ ## Runtime marker
86
+
87
+ ### 直接書換え
88
+
89
+ ```powershell
90
+ cseq marker apply .\src\worker.c --contains "rx=%d" --id RX01 --manifest .\rx01.json
91
+ ```
92
+
93
+ manifestにbefore/after hashを保存し、安全条件が成立するときだけundoできます。
94
+
95
+ ```powershell
96
+ cseq marker undo .\src\worker.c --manifest .\rx01.json
97
+ ```
98
+
99
+ ### Overlay(原本非変更)
100
+
101
+ ```powershell
102
+ cseq marker apply .\src\worker.c `
103
+ --contains "rx=%d" `
104
+ --id RX01 `
105
+ --manifest .\rx01-overlay.json `
106
+ --overlay-output .\cseq-overlay\worker.c
107
+ ```
108
+
109
+ 元sourceは変更されず、marker注入済みコピーだけを生成します。
110
+
111
+ ## Runtime log
112
+
113
+ marker形式:
114
+
115
+ ```text
116
+ [[CSEQ:RX01]] message...
117
+ ```
118
+
119
+ ログ形式は固定ではなくpatternで指定できます。
120
+
121
+ ```powershell
122
+ cseq trace import run.log `
123
+ --pattern "[{cpu}] {time} {message}" `
124
+ --time-format "%H:%M:%S.%f" `
125
+ --json
126
+ ```
127
+
128
+ CPU間で時刻が隣接しているだけでは因果関係とはみなしません。marker、payload、静的path等のevidenceを分離して扱います。
129
+
130
+ ## HTML UI
131
+
132
+ ```powershell
133
+ cseq html C:\path\to\project `
134
+ --entry main `
135
+ --runtime-log run.log `
136
+ --runtime-pattern "[{cpu}] {time} {message}" `
137
+ --runtime-time-format "%H:%M:%S.%f" `
138
+ --output sequence.html `
139
+ --mode auto
140
+ ```
141
+
142
+ UIは大量eventを全DOM化せず、viewport/LOD/Canvas/range bundleを使います。初回リリースでは深い性能チューニングより、汎用性・正しさ・必要UIを優先します。
143
+
144
+ ## 設計上の原則
145
+
146
+ - Silent Dropしない。解析不能領域はPARTIAL/OPAQUE等で明示する。
147
+ - static possible と runtime observed を混同しない。
148
+ - CPUのstatic/build/runtime evidenceを分離する。
149
+ - cross-CPU timestamp adjacencyだけで因果を断定しない。
150
+ - Tree-sitterとClangは代替関係ではなく、Fast Syntax + Selective Semantic Refinementとして併用する。
151
+
152
+ ## 開発時検証
153
+
154
+ ```powershell
155
+ python -m pytest -q
156
+ ```
157
+
158
+ Windows実機で全テスト、Tree-sitter/Clang統合acceptance、clean wheel installを確認してからrelease候補を作成します。
159
+
160
+ ## 同梱ドキュメントの出力
161
+
162
+ インストール済みwheelから、README・設計書(清書版)・実装/検証成績書を1コマンドで取り出せます。
163
+
164
+ ```powershell
165
+ cseq docs readme --output README.md
166
+ cseq docs design --output シーケンス解析_設計書.md
167
+ cseq docs report --output 実装・検証成績書.html
168
+ cseq docs all --output-dir .\cseq-docs
169
+ ```
170
+
171
+ `--output`を省略するとREADME/設計書/成績書の内容を標準出力します。`docs all` は3点を一括で出力します。