telchines 1.0.0__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.
telchines/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Telchines package."""
2
+
3
+ __all__ = ["__version__"]
4
+
5
+ __version__ = "1.0.0"
telchines/__main__.py ADDED
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ from telchines.cli import app
4
+
5
+
6
+ if __name__ == "__main__":
7
+ app()
@@ -0,0 +1 @@
1
+ """Tool adapters for supported verification tools."""
@@ -0,0 +1,130 @@
1
+ from __future__ import annotations
2
+
3
+ import shutil
4
+ import subprocess
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from telchines.adapters.parsing import parse_common_output
10
+ from telchines.errors import AdapterExecutionError
11
+ from telchines.models import AdapterDescriptor, Observation, ToolReference
12
+ from telchines.utils import ensure_directory, utc_now
13
+
14
+
15
+ @dataclass(slots=True)
16
+ class AdapterExecution:
17
+ command: list[str]
18
+ cwd: str
19
+ exit_code: int
20
+ stdout: str
21
+ stderr: str
22
+ log_path: str
23
+ started_at: str
24
+ finished_at: str
25
+ observations: list[Observation]
26
+ summary: str
27
+ artifacts: dict[str, str] = field(default_factory=dict)
28
+ result: dict[str, Any] = field(default_factory=dict)
29
+
30
+
31
+ class ToolAdapter:
32
+ name = "base"
33
+ kind = "tool"
34
+ category = "tool"
35
+ validation_mode = "compile_only"
36
+ binary_names: tuple[str, ...] = ()
37
+ required_binaries: tuple[str, ...] = ()
38
+ supported_workflows: tuple[str, ...] = ()
39
+ artifact_types: tuple[str, ...] = ("log",)
40
+
41
+ def is_available(self) -> bool:
42
+ required = self.required_binaries or self.binary_names
43
+ if not required:
44
+ return True
45
+ return all(shutil.which(binary) for binary in required)
46
+
47
+ def version(self) -> str:
48
+ return "unknown"
49
+
50
+ def build_command(self, project_root: Path, files: list[str], extra_args: list[str] | None = None) -> list[str]:
51
+ raise NotImplementedError
52
+
53
+ def parse_output(self, run_id: str, text: str) -> list[Observation]:
54
+ return parse_common_output(run_id, text)
55
+
56
+ def parse_result(self, project_root: Path, files: list[str], stdout: str, stderr: str, combined: str) -> dict[str, Any]:
57
+ return {}
58
+
59
+ def collect_artifacts(
60
+ self,
61
+ project_root: Path,
62
+ files: list[str],
63
+ artifacts_dir: Path,
64
+ run_id: str,
65
+ stdout: str,
66
+ stderr: str,
67
+ combined: str,
68
+ ) -> dict[str, str]:
69
+ return {}
70
+
71
+ def describe(self, *, enabled: bool = False) -> AdapterDescriptor:
72
+ return AdapterDescriptor(
73
+ name=self.name,
74
+ kind=self.kind,
75
+ category=self.category,
76
+ validation_mode=self.validation_mode,
77
+ binary_names=list(self.binary_names),
78
+ required_binaries=list(self.required_binaries or self.binary_names),
79
+ supported_workflows=list(self.supported_workflows),
80
+ artifact_types=list(self.artifact_types),
81
+ available=self.is_available(),
82
+ enabled=enabled,
83
+ version=self.version(),
84
+ )
85
+
86
+ def run(self, run_id: str, project_root: Path, files: list[str], artifacts_dir: Path, extra_args: list[str] | None = None) -> AdapterExecution:
87
+ command = self.build_command(project_root, files, extra_args)
88
+ if not files:
89
+ raise AdapterExecutionError(f"{self.name} requires at least one input file")
90
+ if not self.is_available():
91
+ binaries = ", ".join(self.binary_names) or self.name
92
+ raise AdapterExecutionError(f"{self.name} is not available on PATH; expected one of: {binaries}")
93
+ started_at = utc_now()
94
+ try:
95
+ process = subprocess.run(command, cwd=project_root, capture_output=True, text=True, check=False)
96
+ except OSError as exc:
97
+ raise AdapterExecutionError(f"failed to execute {self.name}: {exc}") from exc
98
+ finished_at = utc_now()
99
+ ensure_directory(artifacts_dir)
100
+ log_path = artifacts_dir / f"{run_id}.log"
101
+ combined = process.stdout + process.stderr
102
+ log_path.write_text(combined, encoding="utf-8")
103
+ observations = self.parse_output(run_id, combined)
104
+ result = self.parse_result(project_root, files, process.stdout, process.stderr, combined)
105
+ artifacts = {"log_path": str(log_path)}
106
+ artifacts.update(self.collect_artifacts(project_root, files, artifacts_dir, run_id, process.stdout, process.stderr, combined))
107
+ summary = f"{self.name} exited with code {process.returncode}"
108
+ normalized_status = str(result.get("status", "")).strip()
109
+ if normalized_status:
110
+ summary = f"{summary}; status: {normalized_status}"
111
+ if observations:
112
+ summary = f"{summary}; first observation: {observations[0].signature}"
113
+ return AdapterExecution(
114
+ command=command,
115
+ cwd=str(project_root),
116
+ exit_code=process.returncode,
117
+ stdout=process.stdout,
118
+ stderr=process.stderr,
119
+ log_path=str(log_path),
120
+ started_at=started_at,
121
+ finished_at=finished_at,
122
+ observations=observations,
123
+ summary=summary,
124
+ artifacts=artifacts,
125
+ result=result,
126
+ )
127
+
128
+ @property
129
+ def tool_reference(self) -> ToolReference:
130
+ return ToolReference(kind=self.kind, name=self.name, version=self.version())
@@ -0,0 +1,187 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ import subprocess
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from telchines.adapters.base import AdapterExecution, ToolAdapter
9
+ from telchines.errors import AdapterExecutionError
10
+ from telchines.utils import ensure_directory, utc_now
11
+
12
+
13
+ class VerilatorAdapter(ToolAdapter):
14
+ name = "verilator"
15
+ kind = "simulator"
16
+ category = "simulation"
17
+ binary_names = ("verilator",)
18
+ supported_workflows = ("repair_validation",)
19
+ artifact_types = ("log", "stdout", "stderr")
20
+
21
+ def build_command(self, project_root: Path, files: list[str], extra_args: list[str] | None = None) -> list[str]:
22
+ return ["verilator", "--lint-only", *(extra_args or []), *files]
23
+
24
+
25
+ class IcarusAdapter(ToolAdapter):
26
+ name = "iverilog"
27
+ kind = "simulator"
28
+ category = "simulation"
29
+ validation_mode = "compile_and_run"
30
+ binary_names = ("iverilog", "vvp")
31
+ required_binaries = ("iverilog", "vvp")
32
+ supported_workflows = ("repair_validation",)
33
+ artifact_types = ("log", "stdout", "stderr", "executable")
34
+
35
+ def build_command(self, project_root: Path, files: list[str], extra_args: list[str] | None = None) -> list[str]:
36
+ return ["iverilog", "-g2012", *(extra_args or []), *files]
37
+
38
+ def run(self, run_id: str, project_root: Path, files: list[str], artifacts_dir: Path, extra_args: list[str] | None = None) -> AdapterExecution:
39
+ if not files:
40
+ raise AdapterExecutionError(f"{self.name} requires at least one input file")
41
+ if not self.is_available():
42
+ binaries = ", ".join(self.required_binaries or self.binary_names) or self.name
43
+ raise AdapterExecutionError(f"{self.name} is not available on PATH; expected: {binaries}")
44
+
45
+ ensure_directory(artifacts_dir)
46
+ executable_path = artifacts_dir / f"{run_id}.out"
47
+ compile_command = ["iverilog", "-g2012", "-o", str(executable_path), *(extra_args or []), *files]
48
+ compile_started_at = utc_now()
49
+ try:
50
+ compile_process = subprocess.run(compile_command, cwd=project_root, capture_output=True, text=True, check=False)
51
+ except OSError as exc:
52
+ raise AdapterExecutionError(f"failed to execute {self.name} compile step: {exc}") from exc
53
+
54
+ run_command = ["vvp", str(executable_path)]
55
+ run_stdout = ""
56
+ run_stderr = ""
57
+ run_exit_code: int | None = None
58
+ if compile_process.returncode == 0:
59
+ try:
60
+ run_process = subprocess.run(run_command, cwd=project_root, capture_output=True, text=True, check=False)
61
+ except OSError as exc:
62
+ raise AdapterExecutionError(f"failed to execute {self.name} run step: {exc}") from exc
63
+ run_stdout = run_process.stdout
64
+ run_stderr = run_process.stderr
65
+ run_exit_code = run_process.returncode
66
+
67
+ finished_at = utc_now()
68
+ combined = compile_process.stdout + compile_process.stderr + run_stdout + run_stderr
69
+ log_path = artifacts_dir / f"{run_id}.log"
70
+ log_path.write_text(combined, encoding="utf-8")
71
+ observations = self.parse_output(run_id, combined)
72
+ overall_exit_code = compile_process.returncode if compile_process.returncode != 0 else (run_exit_code or 0)
73
+ result = {
74
+ "status": "passed" if overall_exit_code == 0 else "failed",
75
+ "validation_mode": self.validation_mode,
76
+ "compile_command": compile_command,
77
+ "run_command": run_command if compile_process.returncode == 0 else [],
78
+ "compile_exit_code": compile_process.returncode,
79
+ "run_exit_code": run_exit_code,
80
+ "artifact_path": str(executable_path),
81
+ }
82
+ summary = f"{self.name} compile+run exited with code {overall_exit_code}"
83
+ if observations:
84
+ summary = f"{summary}; first observation: {observations[0].signature}"
85
+ return AdapterExecution(
86
+ command=compile_command,
87
+ cwd=str(project_root),
88
+ exit_code=overall_exit_code,
89
+ stdout=compile_process.stdout + run_stdout,
90
+ stderr=compile_process.stderr + run_stderr,
91
+ log_path=str(log_path),
92
+ started_at=compile_started_at,
93
+ finished_at=finished_at,
94
+ observations=observations,
95
+ summary=summary,
96
+ artifacts={"log_path": str(log_path), "compiled_executable": str(executable_path)},
97
+ result=result,
98
+ )
99
+
100
+
101
+ class SlangAdapter(ToolAdapter):
102
+ name = "slang"
103
+ kind = "simulator"
104
+ category = "simulation"
105
+ binary_names = ("slang",)
106
+ supported_workflows = ("repair_validation", "generation_validation")
107
+ artifact_types = ("log", "stdout", "stderr")
108
+
109
+ def build_command(self, project_root: Path, files: list[str], extra_args: list[str] | None = None) -> list[str]:
110
+ return ["slang", "--lint-only", *(extra_args or []), *files]
111
+
112
+
113
+ class VeribleAdapter(ToolAdapter):
114
+ name = "verible"
115
+ kind = "linter"
116
+ category = "lint"
117
+ binary_names = ("verible-verilog-lint",)
118
+ supported_workflows = ("repair_validation",)
119
+ artifact_types = ("log", "stdout", "stderr")
120
+
121
+ def build_command(self, project_root: Path, files: list[str], extra_args: list[str] | None = None) -> list[str]:
122
+ return ["verible-verilog-lint", *(extra_args or []), *files]
123
+
124
+
125
+ class SymbiYosysAdapter(ToolAdapter):
126
+ name = "symbiyosys"
127
+ kind = "formal"
128
+ category = "formal"
129
+ binary_names = ("sby",)
130
+ supported_workflows = ("formal_validation",)
131
+ artifact_types = ("log", "report", "counterexample")
132
+
133
+ def build_command(self, project_root: Path, files: list[str], extra_args: list[str] | None = None) -> list[str]:
134
+ return ["sby", *(extra_args or []), *files]
135
+
136
+ def parse_result(self, project_root: Path, files: list[str], stdout: str, stderr: str, combined: str) -> dict[str, Any]:
137
+ return _parse_symbiyosys_result(project_root, combined)
138
+
139
+
140
+ def _parse_symbiyosys_result(project_root: Path, combined: str) -> dict[str, Any]:
141
+ lowered = combined.lower()
142
+ status = "unknown"
143
+ if re.search(r"\b(status|summary)\s*:\s*passed\b", lowered):
144
+ status = "passed"
145
+ elif re.search(r"\b(status|summary)\s*:\s*failed\b", lowered):
146
+ status = "failed"
147
+ elif "assert failed" in lowered or "counterexample" in lowered:
148
+ status = "failed"
149
+
150
+ property_ids = sorted(
151
+ {
152
+ match.group("name")
153
+ for match in re.finditer(
154
+ r"(?P<name>[A-Za-z_][A-Za-z0-9_$]*)\s+(?:proved|failed|covered|unreached)",
155
+ combined,
156
+ flags=re.IGNORECASE,
157
+ )
158
+ }
159
+ )
160
+ referenced_artifacts = _existing_artifacts(project_root, combined)
161
+ counterexample_paths = [path for path in referenced_artifacts if path.endswith((".vcd", ".fst"))]
162
+ report_paths = [path for path in referenced_artifacts if path.endswith((".json", ".txt", ".log"))]
163
+ return {
164
+ "status": status,
165
+ "property_ids": property_ids,
166
+ "counterexample_paths": counterexample_paths,
167
+ "report_paths": report_paths,
168
+ "referenced_artifacts": referenced_artifacts,
169
+ }
170
+
171
+
172
+ def _existing_artifacts(project_root: Path, combined: str) -> list[str]:
173
+ candidates = re.findall(r"([A-Za-z0-9_./\\\\-]+\.(?:vcd|fst|json|txt|log))", combined)
174
+ artifacts: list[str] = []
175
+ seen: set[str] = set()
176
+ for candidate in candidates:
177
+ path = Path(candidate)
178
+ resolved = path if path.is_absolute() else (project_root / path)
179
+ try:
180
+ normalized = str(resolved.resolve().relative_to(project_root.resolve())).replace("\\", "/")
181
+ except ValueError:
182
+ normalized = str(resolved.resolve()).replace("\\", "/")
183
+ if not resolved.exists() or normalized in seen:
184
+ continue
185
+ seen.add(normalized)
186
+ artifacts.append(normalized)
187
+ return artifacts
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from pathlib import Path
5
+
6
+ from telchines.models import Observation
7
+ from telchines.utils import stable_id
8
+
9
+ GENERIC_PATTERNS = [
10
+ re.compile(r"ERROR:\s+(?P<file>.+?):(?P<line>\d+):\s+(?P<message>.+)"),
11
+ re.compile(r"(?P<file>[^:\n]+):(?P<line>\d+):\s*(?P<severity>error|warning):\s*(?P<message>.+)", re.IGNORECASE),
12
+ ]
13
+
14
+
15
+ def normalize_signature(message: str) -> str:
16
+ text = message.lower()
17
+ if "semicolon" in text:
18
+ return "SV_PARSE_EXPECTED_SEMICOLON"
19
+ if "endmodule" in text:
20
+ return "SV_EXPECTED_ENDMODULE"
21
+ if ("missing end" in text) or ("expecting 'end'" in text) or ("expected end before" in text):
22
+ return "SV_EXPECTED_END"
23
+ if "syntax error" in text:
24
+ return "SV_GENERIC_SYNTAX_ERROR"
25
+ if "undeclared" in text or "unknown identifier" in text:
26
+ return "SV_UNKNOWN_IDENTIFIER"
27
+ if "cannot open" in text or "no such file" in text:
28
+ return "FILE_NOT_FOUND"
29
+ if "timeout" in text:
30
+ return "SIM_TIMEOUT"
31
+ if "assert" in text and "fail" in text:
32
+ return "ASSERTION_FAILURE"
33
+ compact = re.sub(r"[^a-z0-9]+", "_", text.upper()).strip("_")
34
+ return compact[:64] or "UNKNOWN_TOOL_ERROR"
35
+
36
+
37
+ def parse_common_output(run_id: str, text: str, default_type: str = "tool_error") -> list[Observation]:
38
+ observations: list[Observation] = []
39
+ for line_text in text.splitlines():
40
+ for pattern in GENERIC_PATTERNS:
41
+ match = pattern.search(line_text)
42
+ if not match:
43
+ continue
44
+ message = match.group("message").strip()
45
+ observations.append(
46
+ Observation(
47
+ observation_id=stable_id("obs", run_id, line_text),
48
+ run_id=run_id,
49
+ type=default_type,
50
+ signature=normalize_signature(message),
51
+ file=str(Path(match.group("file"))),
52
+ line=int(match.group("line")),
53
+ message=message,
54
+ severity=match.groupdict().get("severity", "error").lower(),
55
+ )
56
+ )
57
+ break
58
+ return observations
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ from telchines.adapters.base import ToolAdapter
4
+ from telchines.adapters.open_tools import IcarusAdapter, SlangAdapter, SymbiYosysAdapter, VeribleAdapter, VerilatorAdapter
5
+
6
+
7
+ class AdapterRegistry:
8
+ def __init__(self) -> None:
9
+ self._adapters: dict[str, ToolAdapter] = {
10
+ "verilator": VerilatorAdapter(),
11
+ "iverilog": IcarusAdapter(),
12
+ "slang": SlangAdapter(),
13
+ "verible": VeribleAdapter(),
14
+ "symbiyosys": SymbiYosysAdapter(),
15
+ }
16
+
17
+ def get(self, name: str) -> ToolAdapter:
18
+ try:
19
+ return self._adapters[name]
20
+ except KeyError as exc:
21
+ raise KeyError(f"unknown adapter: {name}") from exc
22
+
23
+ def list(self, *, category: str | None = None) -> list[ToolAdapter]:
24
+ adapters = list(self._adapters.values())
25
+ if category is not None:
26
+ adapters = [adapter for adapter in adapters if adapter.category == category or adapter.kind == category]
27
+ return sorted(adapters, key=lambda adapter: adapter.name)