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/compile_db.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import shlex
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=True)
|
|
11
|
+
class CompileCommand:
|
|
12
|
+
file: Path
|
|
13
|
+
directory: Path
|
|
14
|
+
arguments: tuple[str, ...]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CompilationDatabase:
|
|
18
|
+
def __init__(self, entries: dict[Path, CompileCommand], source_path: Path) -> None:
|
|
19
|
+
self._entries = entries
|
|
20
|
+
self.source_path = source_path
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def load(cls, path: str | Path) -> "CompilationDatabase":
|
|
24
|
+
p = Path(path).resolve()
|
|
25
|
+
raw = json.loads(p.read_text(encoding="utf-8"))
|
|
26
|
+
if not isinstance(raw, list):
|
|
27
|
+
raise ValueError("compile_commands.json must contain a JSON array")
|
|
28
|
+
entries: dict[Path, CompileCommand] = {}
|
|
29
|
+
for item in raw:
|
|
30
|
+
if not isinstance(item, dict) or "file" not in item:
|
|
31
|
+
continue
|
|
32
|
+
directory = Path(item.get("directory") or p.parent)
|
|
33
|
+
if not directory.is_absolute():
|
|
34
|
+
directory = (p.parent / directory).resolve()
|
|
35
|
+
else:
|
|
36
|
+
directory = directory.resolve()
|
|
37
|
+
file_path = Path(str(item["file"]))
|
|
38
|
+
if not file_path.is_absolute():
|
|
39
|
+
file_path = (directory / file_path).resolve()
|
|
40
|
+
else:
|
|
41
|
+
file_path = file_path.resolve()
|
|
42
|
+
args = item.get("arguments")
|
|
43
|
+
if isinstance(args, list):
|
|
44
|
+
tokens = [str(x) for x in args]
|
|
45
|
+
elif isinstance(item.get("command"), str):
|
|
46
|
+
tokens = shlex.split(str(item["command"]), posix=True)
|
|
47
|
+
else:
|
|
48
|
+
continue
|
|
49
|
+
cleaned = _clean_compile_arguments(tokens, file_path, directory)
|
|
50
|
+
entries[file_path] = CompileCommand(file=file_path, directory=directory, arguments=tuple(cleaned))
|
|
51
|
+
return cls(entries, p)
|
|
52
|
+
|
|
53
|
+
def lookup(self, file: str | Path) -> CompileCommand | None:
|
|
54
|
+
try:
|
|
55
|
+
key = Path(file).resolve()
|
|
56
|
+
except OSError:
|
|
57
|
+
key = Path(file)
|
|
58
|
+
return self._entries.get(key)
|
|
59
|
+
|
|
60
|
+
def __len__(self) -> int:
|
|
61
|
+
return len(self._entries)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _same_path_token(token: str, file_path: Path, directory: Path) -> bool:
|
|
65
|
+
p = Path(token)
|
|
66
|
+
try:
|
|
67
|
+
candidate = (directory / p).resolve() if not p.is_absolute() else p.resolve()
|
|
68
|
+
return candidate == file_path
|
|
69
|
+
except OSError:
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _clean_compile_arguments(tokens: list[str], file_path: Path, directory: Path) -> list[str]:
|
|
74
|
+
"""Keep semantic compile flags while dropping build-output/dependency side effects."""
|
|
75
|
+
if tokens:
|
|
76
|
+
# The first token is normally the compiler executable.
|
|
77
|
+
tokens = tokens[1:]
|
|
78
|
+
out: list[str] = []
|
|
79
|
+
i = 0
|
|
80
|
+
takes_value_and_drop = {"-o", "-MF", "-MT", "-MQ", "-MJ"}
|
|
81
|
+
standalone_drop = {"-c", "-MD", "-MMD", "-MP", "-MG", "-MM", "-M", "-E", "-S"}
|
|
82
|
+
while i < len(tokens):
|
|
83
|
+
tok = tokens[i]
|
|
84
|
+
if tok in takes_value_and_drop:
|
|
85
|
+
i += 2
|
|
86
|
+
continue
|
|
87
|
+
if tok in standalone_drop:
|
|
88
|
+
i += 1
|
|
89
|
+
continue
|
|
90
|
+
if any(tok.startswith(prefix) and tok != prefix for prefix in ("-o", "-MF", "-MT", "-MQ", "-MJ")):
|
|
91
|
+
i += 1
|
|
92
|
+
continue
|
|
93
|
+
if _same_path_token(tok, file_path, directory):
|
|
94
|
+
i += 1
|
|
95
|
+
continue
|
|
96
|
+
out.append(tok)
|
|
97
|
+
i += 1
|
|
98
|
+
return out
|
cseq/config.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import tomllib
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class CpuPreprocessorRule:
|
|
10
|
+
when_defined: str
|
|
11
|
+
assign_group: str | None = None
|
|
12
|
+
assign_cpu: str | None = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, slots=True)
|
|
16
|
+
class CpuPathRule:
|
|
17
|
+
glob: str
|
|
18
|
+
assign_group: str | None = None
|
|
19
|
+
assign_cpu: str | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True, slots=True)
|
|
23
|
+
class CpuOverrideRule:
|
|
24
|
+
file: str
|
|
25
|
+
assign_group: str | None = None
|
|
26
|
+
assign_cpu: str | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(slots=True)
|
|
30
|
+
class CseqConfig:
|
|
31
|
+
path: Path | None = None
|
|
32
|
+
cpu_groups: dict[str, tuple[str, ...]] = field(default_factory=dict)
|
|
33
|
+
preprocessor_rules: list[CpuPreprocessorRule] = field(default_factory=list)
|
|
34
|
+
path_rules: list[CpuPathRule] = field(default_factory=list)
|
|
35
|
+
override_rules: list[CpuOverrideRule] = field(default_factory=list)
|
|
36
|
+
configurations: dict[str, tuple[str, ...]] = field(default_factory=dict)
|
|
37
|
+
plugins_enabled: tuple[str, ...] = ()
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def empty(cls) -> "CseqConfig":
|
|
41
|
+
return cls()
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def load(cls, path: str | Path) -> "CseqConfig":
|
|
45
|
+
p = Path(path)
|
|
46
|
+
data = tomllib.loads(p.read_text(encoding="utf-8"))
|
|
47
|
+
cfg = cls(path=p.resolve())
|
|
48
|
+
for name, value in (data.get("cpu_groups") or {}).items():
|
|
49
|
+
cfg.cpu_groups[str(name)] = tuple(str(x) for x in (value or {}).get("members", []))
|
|
50
|
+
cpu_rules = data.get("cpu_rules") or {}
|
|
51
|
+
for r in cpu_rules.get("preprocessor", []) or []:
|
|
52
|
+
cfg.preprocessor_rules.append(CpuPreprocessorRule(str(r["when_defined"]), _opt(r, "assign_group"), _opt(r, "assign_cpu")))
|
|
53
|
+
for r in cpu_rules.get("path", []) or []:
|
|
54
|
+
cfg.path_rules.append(CpuPathRule(str(r["glob"]), _opt(r, "assign_group"), _opt(r, "assign_cpu")))
|
|
55
|
+
for r in cpu_rules.get("override", []) or []:
|
|
56
|
+
cfg.override_rules.append(CpuOverrideRule(str(r["file"]), _opt(r, "assign_group"), _opt(r, "assign_cpu")))
|
|
57
|
+
for name, value in (data.get("configurations") or {}).items():
|
|
58
|
+
cfg.configurations[str(name)] = tuple(str(x) for x in (value or {}).get("defines", []))
|
|
59
|
+
plugins = data.get("plugins") or {}
|
|
60
|
+
cfg.plugins_enabled = tuple(str(x) for x in (plugins.get("enabled") or []))
|
|
61
|
+
return cfg
|
|
62
|
+
|
|
63
|
+
def defines_for(self, configuration: str | None, extra_defines: tuple[str, ...] = ()) -> tuple[str, ...]:
|
|
64
|
+
merged: list[str] = []
|
|
65
|
+
if configuration:
|
|
66
|
+
if configuration not in self.configurations:
|
|
67
|
+
raise ValueError(f"unknown configuration: {configuration}")
|
|
68
|
+
merged.extend(self.configurations[configuration])
|
|
69
|
+
merged.extend(extra_defines)
|
|
70
|
+
return tuple(dict.fromkeys(merged))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _opt(mapping: dict, key: str) -> str | None:
|
|
74
|
+
value = mapping.get(key)
|
|
75
|
+
return str(value) if value is not None else None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def validate_config(config: CseqConfig) -> list[dict[str, str]]:
|
|
79
|
+
"""Return structured configuration diagnostics without mutating the config."""
|
|
80
|
+
issues: list[dict[str, str]] = []
|
|
81
|
+
known_groups = set(config.cpu_groups)
|
|
82
|
+
for kind, rules in (
|
|
83
|
+
("preprocessor", config.preprocessor_rules),
|
|
84
|
+
("path", config.path_rules),
|
|
85
|
+
("override", config.override_rules),
|
|
86
|
+
):
|
|
87
|
+
seen: set[tuple[str, str | None, str | None]] = set()
|
|
88
|
+
for rule in rules:
|
|
89
|
+
selector = getattr(rule, "when_defined", None) or getattr(rule, "glob", None) or getattr(rule, "file", None) or ""
|
|
90
|
+
key = (str(selector), getattr(rule, "assign_group", None), getattr(rule, "assign_cpu", None))
|
|
91
|
+
if key in seen:
|
|
92
|
+
issues.append({"severity": "warning", "code": "DUPLICATE_RULE", "message": f"duplicate {kind} rule: {selector}"})
|
|
93
|
+
seen.add(key)
|
|
94
|
+
group = getattr(rule, "assign_group", None)
|
|
95
|
+
cpu = getattr(rule, "assign_cpu", None)
|
|
96
|
+
if group and group not in known_groups:
|
|
97
|
+
issues.append({"severity": "error", "code": "UNKNOWN_CPU_GROUP", "message": f"{kind} rule references unknown group: {group}"})
|
|
98
|
+
if group and cpu:
|
|
99
|
+
issues.append({"severity": "error", "code": "AMBIGUOUS_ASSIGNMENT", "message": f"{kind} rule assigns both group and cpu: {selector}"})
|
|
100
|
+
if not group and not cpu:
|
|
101
|
+
issues.append({"severity": "error", "code": "MISSING_ASSIGNMENT", "message": f"{kind} rule has no assignment: {selector}"})
|
|
102
|
+
for name, members in config.cpu_groups.items():
|
|
103
|
+
if not members:
|
|
104
|
+
issues.append({"severity": "warning", "code": "EMPTY_CPU_GROUP", "message": f"CPU group has no members: {name}"})
|
|
105
|
+
return issues
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def default_config_text(*, compile_commands_present: bool = False) -> str:
|
|
109
|
+
lines = [
|
|
110
|
+
'# cseq configuration',
|
|
111
|
+
'# Generated conservatively. Company-specific CPU/RTOS semantics are not guessed.',
|
|
112
|
+
'',
|
|
113
|
+
'[project]',
|
|
114
|
+
'entries = ["main"]',
|
|
115
|
+
'',
|
|
116
|
+
'[parser]',
|
|
117
|
+
'dialect = "auto"',
|
|
118
|
+
f'compile_commands = "{"auto" if compile_commands_present else "none"}"',
|
|
119
|
+
'tolerant = true',
|
|
120
|
+
'',
|
|
121
|
+
'[analysis]',
|
|
122
|
+
'indirect_calls = true',
|
|
123
|
+
'',
|
|
124
|
+
'[sequence]',
|
|
125
|
+
'external_calls = "self"',
|
|
126
|
+
'',
|
|
127
|
+
'[html]',
|
|
128
|
+
'mode = "auto"',
|
|
129
|
+
'',
|
|
130
|
+
'[plugins]',
|
|
131
|
+
'enabled = []',
|
|
132
|
+
'',
|
|
133
|
+
'# Example only; uncomment and rename if needed.',
|
|
134
|
+
'# [cpu_groups.CPU_GROUP_A]',
|
|
135
|
+
'# members = ["CPU_A1", "CPU_A2"]',
|
|
136
|
+
'',
|
|
137
|
+
]
|
|
138
|
+
return '\n'.join(lines)
|
cseq/cpu_rules.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import PurePosixPath
|
|
4
|
+
|
|
5
|
+
from .config import CseqConfig
|
|
6
|
+
from .model import CpuEvidenceLayer, CpuEvidenceRecord, CpuValueKind, ProjectIndex
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def apply_cpu_rules(index: ProjectIndex, config: CseqConfig, defined_macros: tuple[str, ...]) -> None:
|
|
10
|
+
if not config.path_rules and not config.override_rules and not config.preprocessor_rules:
|
|
11
|
+
return
|
|
12
|
+
defined_names = {_macro_name(x) for x in defined_macros}
|
|
13
|
+
|
|
14
|
+
for fn in index.functions():
|
|
15
|
+
path = fn.source_path.replace("\\", "/")
|
|
16
|
+
static_records: list[CpuEvidenceRecord] = []
|
|
17
|
+
for rule in config.path_rules:
|
|
18
|
+
if PurePosixPath(path).match(rule.glob):
|
|
19
|
+
rec = _record(fn.qualified_id, CpuEvidenceLayer.STATIC_AFFINITY, rule.assign_group, rule.assign_cpu, f"path:{rule.glob}")
|
|
20
|
+
if rec:
|
|
21
|
+
static_records.append(rec)
|
|
22
|
+
|
|
23
|
+
# An explicit file override wins *within the static-affinity layer*.
|
|
24
|
+
override_records: list[CpuEvidenceRecord] = []
|
|
25
|
+
for rule in config.override_rules:
|
|
26
|
+
if path == rule.file.replace("\\", "/"):
|
|
27
|
+
rec = _record(fn.qualified_id, CpuEvidenceLayer.STATIC_AFFINITY, rule.assign_group, rule.assign_cpu, f"override:{rule.file}", True)
|
|
28
|
+
if rec:
|
|
29
|
+
override_records.append(rec)
|
|
30
|
+
index.cpu_evidence.extend(override_records or static_records)
|
|
31
|
+
|
|
32
|
+
for rule in config.preprocessor_rules:
|
|
33
|
+
if rule.when_defined in defined_names:
|
|
34
|
+
rec = _record(fn.qualified_id, CpuEvidenceLayer.BUILD_DOMAIN, rule.assign_group, rule.assign_cpu, f"define:{rule.when_defined}")
|
|
35
|
+
if rec:
|
|
36
|
+
index.cpu_evidence.append(rec)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _record(function_id: str, layer: CpuEvidenceLayer, group: str | None, cpu: str | None, provenance: str, override: bool = False) -> CpuEvidenceRecord | None:
|
|
40
|
+
if cpu:
|
|
41
|
+
return CpuEvidenceRecord(function_id, layer, CpuValueKind.CPU, cpu, provenance, override)
|
|
42
|
+
if group:
|
|
43
|
+
return CpuEvidenceRecord(function_id, layer, CpuValueKind.CPU_GROUP, group, provenance, override)
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _macro_name(value: str) -> str:
|
|
48
|
+
return value.split("=", 1)[0]
|
cseq/dependencies.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from hashlib import sha256
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
from .model import TranslationUnit
|
|
10
|
+
from .source_index import SourceFingerprintIndex
|
|
11
|
+
|
|
12
|
+
_INCLUDE_RE = re.compile(r'^\s*#\s*include\s*([<"])([^>"]+)[>"]', re.MULTILINE)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, slots=True)
|
|
16
|
+
class IncludeDependency:
|
|
17
|
+
spelling: str
|
|
18
|
+
resolved_path: str | None
|
|
19
|
+
content_hash: str | None
|
|
20
|
+
quoted: bool = False
|
|
21
|
+
absolute_path: str | None = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _hash_file(path: Path) -> str:
|
|
25
|
+
h = sha256()
|
|
26
|
+
with path.open('rb') as f:
|
|
27
|
+
for chunk in iter(lambda: f.read(1024 * 1024), b''):
|
|
28
|
+
h.update(chunk)
|
|
29
|
+
return h.hexdigest()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _include_dirs(tu: TranslationUnit) -> list[Path]:
|
|
33
|
+
result: list[Path] = []
|
|
34
|
+
args = list(tu.arguments)
|
|
35
|
+
i = 0
|
|
36
|
+
while i < len(args):
|
|
37
|
+
arg = args[i]
|
|
38
|
+
value: str | None = None
|
|
39
|
+
if arg in {'-I', '-isystem'} and i + 1 < len(args):
|
|
40
|
+
value = args[i + 1]
|
|
41
|
+
i += 1
|
|
42
|
+
elif arg.startswith('-I') and len(arg) > 2:
|
|
43
|
+
value = arg[2:]
|
|
44
|
+
elif arg.startswith('-isystem') and len(arg) > len('-isystem'):
|
|
45
|
+
value = arg[len('-isystem'):]
|
|
46
|
+
if value:
|
|
47
|
+
p = Path(value)
|
|
48
|
+
if not p.is_absolute():
|
|
49
|
+
p = (tu.working_directory or tu.source.path.parent) / p
|
|
50
|
+
result.append(p.resolve())
|
|
51
|
+
i += 1
|
|
52
|
+
return result
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _resolve_include(name: str, quoted: bool, current: Path, tu: TranslationUnit) -> Path | None:
|
|
56
|
+
candidates: list[Path] = []
|
|
57
|
+
if quoted:
|
|
58
|
+
candidates.append(current.parent / name)
|
|
59
|
+
candidates.extend(p / name for p in _include_dirs(tu))
|
|
60
|
+
if tu.working_directory is not None:
|
|
61
|
+
candidates.append(tu.working_directory / name)
|
|
62
|
+
for candidate in candidates:
|
|
63
|
+
try:
|
|
64
|
+
if candidate.is_file():
|
|
65
|
+
return candidate.resolve()
|
|
66
|
+
except OSError:
|
|
67
|
+
continue
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _context_hash(tu: TranslationUnit) -> str:
|
|
72
|
+
payload = {
|
|
73
|
+
'working_directory': str((tu.working_directory or tu.source.path.parent).resolve()),
|
|
74
|
+
'arguments': list(tu.arguments),
|
|
75
|
+
'configuration': tu.configuration_name,
|
|
76
|
+
}
|
|
77
|
+
return sha256(json.dumps(payload, sort_keys=True, separators=(',', ':')).encode('utf-8')).hexdigest()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def collect_include_dependencies(
|
|
81
|
+
tu: TranslationUnit,
|
|
82
|
+
fingerprint_index: SourceFingerprintIndex | None = None,
|
|
83
|
+
) -> tuple[IncludeDependency, ...]:
|
|
84
|
+
"""Collect reachable textual include dependencies for cache invalidation."""
|
|
85
|
+
visited: set[Path] = set()
|
|
86
|
+
records: dict[tuple[str, str | None], IncludeDependency] = {}
|
|
87
|
+
|
|
88
|
+
def visit(path: Path) -> None:
|
|
89
|
+
try:
|
|
90
|
+
real = path.resolve()
|
|
91
|
+
except OSError:
|
|
92
|
+
real = path
|
|
93
|
+
if real in visited:
|
|
94
|
+
return
|
|
95
|
+
visited.add(real)
|
|
96
|
+
try:
|
|
97
|
+
if real == tu.source.path.resolve() and tu.source_text is not None:
|
|
98
|
+
text = tu.source_text
|
|
99
|
+
else:
|
|
100
|
+
text = real.read_text(encoding='utf-8', errors='replace')
|
|
101
|
+
except OSError:
|
|
102
|
+
return
|
|
103
|
+
for m in _INCLUDE_RE.finditer(text):
|
|
104
|
+
quoted = m.group(1) == '"'
|
|
105
|
+
spelling = m.group(2).strip()
|
|
106
|
+
resolved = _resolve_include(spelling, quoted, real, tu)
|
|
107
|
+
if resolved is None:
|
|
108
|
+
rec = IncludeDependency(
|
|
109
|
+
spelling=spelling, resolved_path=None, content_hash=None,
|
|
110
|
+
quoted=quoted, absolute_path=None,
|
|
111
|
+
)
|
|
112
|
+
records[(spelling, None)] = rec
|
|
113
|
+
continue
|
|
114
|
+
try:
|
|
115
|
+
rel = resolved.relative_to(tu.working_directory).as_posix() if tu.working_directory else str(resolved)
|
|
116
|
+
except ValueError:
|
|
117
|
+
rel = str(resolved)
|
|
118
|
+
digest = fingerprint_index.content_hash(resolved) if fingerprint_index is not None else _hash_file(resolved)
|
|
119
|
+
rec = IncludeDependency(
|
|
120
|
+
spelling=spelling, resolved_path=rel, content_hash=digest,
|
|
121
|
+
quoted=quoted, absolute_path=str(resolved),
|
|
122
|
+
)
|
|
123
|
+
records[(spelling, rel)] = rec
|
|
124
|
+
visit(resolved)
|
|
125
|
+
|
|
126
|
+
visit(tu.source.path)
|
|
127
|
+
return tuple(sorted(records.values(), key=lambda r: (r.resolved_path or '', r.spelling)))
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _fingerprint(deps: tuple[IncludeDependency, ...]) -> str:
|
|
131
|
+
h = sha256()
|
|
132
|
+
for dep in deps:
|
|
133
|
+
h.update(dep.spelling.encode('utf-8', errors='surrogatepass'))
|
|
134
|
+
h.update(b'\0')
|
|
135
|
+
h.update((dep.resolved_path or '<unresolved>').encode('utf-8', errors='surrogatepass'))
|
|
136
|
+
h.update(b'\0')
|
|
137
|
+
h.update((dep.content_hash or '<missing>').encode('ascii'))
|
|
138
|
+
h.update(b'\n')
|
|
139
|
+
return h.hexdigest()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _snapshot_records(deps: tuple[IncludeDependency, ...]) -> list[dict]:
|
|
143
|
+
return [
|
|
144
|
+
{
|
|
145
|
+
'spelling': d.spelling,
|
|
146
|
+
'resolved_path': d.resolved_path,
|
|
147
|
+
'absolute_path': d.absolute_path,
|
|
148
|
+
'content_hash': d.content_hash,
|
|
149
|
+
'quoted': d.quoted,
|
|
150
|
+
}
|
|
151
|
+
for d in deps
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _snapshot_valid(tu: TranslationUnit, records: list[dict], index: SourceFingerprintIndex) -> bool:
|
|
156
|
+
for rec in records:
|
|
157
|
+
spelling = str(rec.get('spelling') or '')
|
|
158
|
+
quoted = bool(rec.get('quoted'))
|
|
159
|
+
absolute = rec.get('absolute_path')
|
|
160
|
+
old_hash = rec.get('content_hash')
|
|
161
|
+
if absolute:
|
|
162
|
+
path = Path(str(absolute))
|
|
163
|
+
try:
|
|
164
|
+
if not path.is_file() or index.content_hash(path) != old_hash:
|
|
165
|
+
return False
|
|
166
|
+
except OSError:
|
|
167
|
+
return False
|
|
168
|
+
else:
|
|
169
|
+
# Missing includes are part of cache identity. If they become
|
|
170
|
+
# resolvable, invalidate the old snapshot without reading source.
|
|
171
|
+
if _resolve_include(spelling, quoted, tu.source.path, tu) is not None:
|
|
172
|
+
return False
|
|
173
|
+
return True
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def include_dependency_fingerprint(
|
|
177
|
+
tu: TranslationUnit,
|
|
178
|
+
fingerprint_index: SourceFingerprintIndex | None = None,
|
|
179
|
+
) -> str:
|
|
180
|
+
if fingerprint_index is not None:
|
|
181
|
+
context_hash = _context_hash(tu)
|
|
182
|
+
snapshot = fingerprint_index.dependency_snapshot(
|
|
183
|
+
tu.source.project_relative_path, context_hash, tu.source.content_hash
|
|
184
|
+
)
|
|
185
|
+
if snapshot is not None:
|
|
186
|
+
fingerprint, records = snapshot
|
|
187
|
+
if _snapshot_valid(tu, records, fingerprint_index):
|
|
188
|
+
fingerprint_index.note_dependency_hit()
|
|
189
|
+
return fingerprint
|
|
190
|
+
fingerprint_index.note_dependency_miss()
|
|
191
|
+
deps = collect_include_dependencies(tu, fingerprint_index)
|
|
192
|
+
fingerprint = _fingerprint(deps)
|
|
193
|
+
fingerprint_index.put_dependency_snapshot(
|
|
194
|
+
tu.source.project_relative_path,
|
|
195
|
+
context_hash,
|
|
196
|
+
tu.source.content_hash,
|
|
197
|
+
fingerprint,
|
|
198
|
+
_snapshot_records(deps),
|
|
199
|
+
)
|
|
200
|
+
fingerprint_index.commit()
|
|
201
|
+
return fingerprint
|
|
202
|
+
return _fingerprint(collect_include_dependencies(tu))
|
cseq/docs.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from importlib import resources
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import sys
|
|
5
|
+
_DOCS={"readme":("README.md","README.md"),"design":("design.md","シーケンス解析_設計書.md"),"report":("verification_report.html","実装・検証成績書.html")}
|
|
6
|
+
def document_bytes(kind:str)->bytes:
|
|
7
|
+
try: resource_name,_=_DOCS[kind]
|
|
8
|
+
except KeyError as exc: raise ValueError(f"unknown document kind: {kind}") from exc
|
|
9
|
+
return resources.files("cseq").joinpath("resources",resource_name).read_bytes()
|
|
10
|
+
def export_document(kind:str,output:str|Path|None=None)->Path|None:
|
|
11
|
+
data=document_bytes(kind)
|
|
12
|
+
if output is None:
|
|
13
|
+
if hasattr(sys.stdout,"buffer"): sys.stdout.buffer.write(data)
|
|
14
|
+
else: sys.stdout.write(data.decode("utf-8"))
|
|
15
|
+
return None
|
|
16
|
+
out=Path(output); out.parent.mkdir(parents=True,exist_ok=True); out.write_bytes(data); return out
|
|
17
|
+
def export_all(output_dir:str|Path)->list[Path]:
|
|
18
|
+
root=Path(output_dir); root.mkdir(parents=True,exist_ok=True); out=[]
|
|
19
|
+
for kind,(_,filename) in _DOCS.items():
|
|
20
|
+
p=root/filename; export_document(kind,p); out.append(p)
|
|
21
|
+
return out
|
cseq/dwarf.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import bisect
|
|
6
|
+
import re
|
|
7
|
+
import subprocess
|
|
8
|
+
import shutil
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
from .model import DwarfEvidenceRecord, ProjectIndex
|
|
12
|
+
|
|
13
|
+
_LINE_RE = re.compile(r"^(.+?)\s+(\d+|-)\s+0x([0-9A-Fa-f]+)(?:\s+.*)?$")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True, slots=True)
|
|
17
|
+
class DwarfLineEntry:
|
|
18
|
+
file: str
|
|
19
|
+
line: int | None
|
|
20
|
+
address: int
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True, slots=True)
|
|
24
|
+
class DwarfLineIndex:
|
|
25
|
+
path: str
|
|
26
|
+
entries: tuple[DwarfLineEntry, ...]
|
|
27
|
+
|
|
28
|
+
def resolve_address(self, runtime_address: int, *, load_bias: int = 0) -> DwarfLineEntry | None:
|
|
29
|
+
address = int(runtime_address) - int(load_bias)
|
|
30
|
+
starts = [e.address for e in self.entries]
|
|
31
|
+
pos = bisect.bisect_right(starts, address) - 1
|
|
32
|
+
if pos < 0:
|
|
33
|
+
return None
|
|
34
|
+
entry = self.entries[pos]
|
|
35
|
+
if entry.line is None:
|
|
36
|
+
return None
|
|
37
|
+
if pos + 1 < len(self.entries) and address >= self.entries[pos + 1].address:
|
|
38
|
+
return None
|
|
39
|
+
return entry
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _existing_executable_path(path: str | Path) -> Path:
|
|
43
|
+
p = Path(path)
|
|
44
|
+
if p.exists():
|
|
45
|
+
return p
|
|
46
|
+
if os.name == "nt":
|
|
47
|
+
candidate = Path(str(p) + ".exe")
|
|
48
|
+
if candidate.exists():
|
|
49
|
+
return candidate
|
|
50
|
+
return p
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def load_dwarf_lines(path: str | Path, *, readelf: str = "readelf") -> DwarfLineIndex:
|
|
54
|
+
p = _existing_executable_path(path)
|
|
55
|
+
is_pe = p.read_bytes()[:2] == b"MZ"
|
|
56
|
+
if is_pe:
|
|
57
|
+
tool = shutil.which("objdump") or shutil.which("llvm-objdump") or "objdump"
|
|
58
|
+
command = [tool, "--dwarf=decodedline", str(p)]
|
|
59
|
+
else:
|
|
60
|
+
tool = shutil.which(readelf) or (shutil.which("llvm-readelf") if readelf == "readelf" else None) or readelf
|
|
61
|
+
command = [tool, "--debug-dump=decodedline", str(p)]
|
|
62
|
+
proc = subprocess.run(
|
|
63
|
+
command,
|
|
64
|
+
stdout=subprocess.PIPE,
|
|
65
|
+
stderr=subprocess.PIPE,
|
|
66
|
+
text=True,
|
|
67
|
+
check=False,
|
|
68
|
+
)
|
|
69
|
+
if proc.returncode != 0:
|
|
70
|
+
raise RuntimeError(f"DWARF line reader failed for {p}: {proc.stderr.strip()}")
|
|
71
|
+
entries: list[DwarfLineEntry] = []
|
|
72
|
+
for raw in proc.stdout.splitlines():
|
|
73
|
+
m = _LINE_RE.match(raw.rstrip())
|
|
74
|
+
if not m:
|
|
75
|
+
continue
|
|
76
|
+
file = m.group(1).strip()
|
|
77
|
+
line = None if m.group(2) == "-" else int(m.group(2))
|
|
78
|
+
entries.append(DwarfLineEntry(file=file, line=line, address=int(m.group(3), 16)))
|
|
79
|
+
entries.sort(key=lambda x: x.address)
|
|
80
|
+
return DwarfLineIndex(str(p), tuple(entries))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def apply_dwarf_evidence(index: ProjectIndex, dwarf: DwarfLineIndex) -> list[DwarfEvidenceRecord]:
|
|
84
|
+
"""Bind DWARF line rows to source functions only when file/range is unambiguous."""
|
|
85
|
+
functions = list(index.functions())
|
|
86
|
+
out: list[DwarfEvidenceRecord] = []
|
|
87
|
+
seen: set[tuple[str, int]] = set()
|
|
88
|
+
for entry in dwarf.entries:
|
|
89
|
+
if entry.line is None:
|
|
90
|
+
continue
|
|
91
|
+
basename = Path(entry.file).name
|
|
92
|
+
matches = [
|
|
93
|
+
f for f in functions
|
|
94
|
+
if Path(f.source_path).name == basename
|
|
95
|
+
and f.source_range.start_line is not None
|
|
96
|
+
and f.source_range.end_line is not None
|
|
97
|
+
and f.source_range.start_line <= entry.line <= f.source_range.end_line
|
|
98
|
+
]
|
|
99
|
+
if len(matches) != 1:
|
|
100
|
+
continue
|
|
101
|
+
fn = matches[0]
|
|
102
|
+
key = (fn.qualified_id, entry.address)
|
|
103
|
+
if key in seen:
|
|
104
|
+
continue
|
|
105
|
+
seen.add(key)
|
|
106
|
+
out.append(DwarfEvidenceRecord(
|
|
107
|
+
function_id=fn.qualified_id,
|
|
108
|
+
source_path=fn.source_path,
|
|
109
|
+
line=entry.line,
|
|
110
|
+
address=entry.address,
|
|
111
|
+
dwarf_file=entry.file,
|
|
112
|
+
provenance=f"dwarf:{dwarf.path}",
|
|
113
|
+
))
|
|
114
|
+
index.dwarf_evidence.extend(out)
|
|
115
|
+
return out
|