esbmc_ai 0.5.2.dev2__tar.gz → 0.5.2.dev4__tar.gz
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.
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/PKG-INFO +1 -1
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/esbmc_ai/__about__.py +1 -1
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/esbmc_ai/ai_models.py +15 -8
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/esbmc_ai/chats/key_template_renderer.py +37 -2
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/esbmc_ai/commands/fix_code_command.py +2 -3
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/esbmc_ai/log_handlers.py +1 -1
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/esbmc_ai/solution.py +145 -127
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/esbmc_ai/verifiers/esbmc.py +4 -4
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/.gitignore +0 -0
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/CLA.md +0 -0
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/LICENSE +0 -0
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/README.md +0 -0
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/esbmc_ai/__init__.py +0 -0
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/esbmc_ai/__main__.py +0 -0
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/esbmc_ai/addon_loader.py +0 -0
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/esbmc_ai/base_component.py +0 -0
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/esbmc_ai/chat_command.py +0 -0
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/esbmc_ai/chats/__init__.py +0 -0
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/esbmc_ai/chats/solution_generator.py +0 -0
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/esbmc_ai/chats/template_funcs.py +0 -0
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/esbmc_ai/chats/template_key_provider.py +0 -0
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/esbmc_ai/command_result.py +0 -0
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/esbmc_ai/commands/__init__.py +0 -0
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/esbmc_ai/commands/debug_config.py +0 -0
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/esbmc_ai/commands/exit_command.py +0 -0
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/esbmc_ai/commands/help_command.py +0 -0
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/esbmc_ai/commands/help_config.py +0 -0
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/esbmc_ai/commands/license_command.py +0 -0
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/esbmc_ai/component_manager.py +0 -0
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/esbmc_ai/config.py +0 -0
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/esbmc_ai/issue.py +0 -0
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/esbmc_ai/loading_widget.py +0 -0
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/esbmc_ai/log_categories.py +0 -0
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/esbmc_ai/log_utils.py +0 -0
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/esbmc_ai/program_trace.py +0 -0
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/esbmc_ai/prompt_utils.py +0 -0
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/esbmc_ai/singleton.py +0 -0
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/esbmc_ai/verifier_output.py +0 -0
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/esbmc_ai/verifiers/__init__.py +0 -0
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/esbmc_ai/verifiers/base_source_verifier.py +0 -0
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/esbmc_ai/verifiers/clang.py +0 -0
- {esbmc_ai-0.5.2.dev2 → esbmc_ai-0.5.2.dev4}/pyproject.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: esbmc_ai
|
|
3
|
-
Version: 0.5.2.
|
|
3
|
+
Version: 0.5.2.dev4
|
|
4
4
|
Summary: LLM driven development and automatic repair kit.
|
|
5
5
|
Project-URL: Documentation, https://github.com/esbmc/esbmc-ai/wiki
|
|
6
6
|
Project-URL: Homepage, https://github.com/esbmc/esbmc-ai
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
from uuid import UUID
|
|
5
|
-
from langchain_core.messages import BaseMessage
|
|
5
|
+
from langchain_core.messages import AIMessage, BaseMessage
|
|
6
6
|
from langchain_core.outputs import ChatGeneration, Generation, LLMResult
|
|
7
7
|
from typing_extensions import override
|
|
8
8
|
import structlog
|
|
@@ -26,12 +26,17 @@ class LoggingCallbackHandler(BaseCallbackHandler):
|
|
|
26
26
|
category=LogCategories.CHAT,
|
|
27
27
|
prefix_name=ai_model,
|
|
28
28
|
)
|
|
29
|
+
# Track last printed message index per group: {group_idx: last_msg_idx}
|
|
30
|
+
self._last_printed_idx: dict[int, int] = {}
|
|
29
31
|
|
|
30
32
|
@staticmethod
|
|
31
33
|
def _get_msg_formatted(
|
|
32
34
|
group_idx: int, msg_idx: int, msg: BaseMessage | Generation
|
|
33
35
|
) -> str:
|
|
34
|
-
|
|
36
|
+
base: str = f"MSG {group_idx}-{msg_idx} {msg.type.capitalize()}:"
|
|
37
|
+
if isinstance(msg, AIMessage) and msg.tool_calls:
|
|
38
|
+
return f"{base} <message-body>{msg.text}</message-body>\nTool Call: {msg.tool_calls}"
|
|
39
|
+
return f"{base} <message-body>{msg.text}</message-body>"
|
|
35
40
|
|
|
36
41
|
# @override
|
|
37
42
|
# def on_llm_start(
|
|
@@ -134,12 +139,16 @@ class LoggingCallbackHandler(BaseCallbackHandler):
|
|
|
134
139
|
_ = run_id, parent_run_id, tags, metadata, kwargs
|
|
135
140
|
self.logger.debug("=" * 80)
|
|
136
141
|
self.logger.debug("Invoke Chat Model LLM")
|
|
137
|
-
for
|
|
142
|
+
for group_idx, msg_group in enumerate(messages):
|
|
143
|
+
last_printed = self._last_printed_idx.get(group_idx, -1)
|
|
138
144
|
self.logger.debug("=" * 80)
|
|
139
|
-
for msg_idx, msg in enumerate(
|
|
140
|
-
|
|
145
|
+
for msg_idx, msg in enumerate(
|
|
146
|
+
msg_group[last_printed + 1 :], start=last_printed + 1
|
|
147
|
+
):
|
|
148
|
+
self.logger.debug(self._get_msg_formatted(group_idx, msg_idx, msg))
|
|
141
149
|
if msg_idx < len(msg_group) - 1:
|
|
142
150
|
self.logger.debug("-" * 80)
|
|
151
|
+
self._last_printed_idx[group_idx] = len(msg_group) - 1
|
|
143
152
|
|
|
144
153
|
|
|
145
154
|
class AIModel:
|
|
@@ -154,9 +163,7 @@ class AIModel:
|
|
|
154
163
|
temperature: float | None = None,
|
|
155
164
|
url: str | None = None,
|
|
156
165
|
) -> BaseChatModel:
|
|
157
|
-
handler: BaseCallbackHandler = LoggingCallbackHandler(
|
|
158
|
-
ai_model=f"{provider}:{model}"
|
|
159
|
-
)
|
|
166
|
+
handler: BaseCallbackHandler = LoggingCallbackHandler(ai_model=model)
|
|
160
167
|
|
|
161
168
|
chat_model: BaseChatModel = init_chat_model(
|
|
162
169
|
model=model,
|
|
@@ -3,16 +3,51 @@
|
|
|
3
3
|
"""Template renderer that integrates TemplateKeyProvider with ChatPromptTemplate."""
|
|
4
4
|
|
|
5
5
|
from typing import Any, Sequence, override
|
|
6
|
+
from jinja2.sandbox import SandboxedEnvironment
|
|
6
7
|
from langchain_core.messages import BaseMessage
|
|
7
8
|
from langchain_core.prompt_values import ChatPromptValue
|
|
8
9
|
from langchain_core.prompts import ChatPromptTemplate
|
|
9
10
|
from langchain_core.prompts.chat import MessageLikeRepresentation
|
|
10
|
-
from langchain_core.prompts.string import PromptTemplateFormat
|
|
11
|
-
from pydantic import PrivateAttr
|
|
11
|
+
from langchain_core.prompts.string import PromptTemplateFormat, DEFAULT_FORMATTER_MAPPING
|
|
12
|
+
from pydantic import PrivateAttr, BaseModel
|
|
12
13
|
from esbmc_ai.chats.template_key_provider import TemplateKeyProvider
|
|
13
14
|
from .template_funcs import get_func_mapping
|
|
14
15
|
|
|
15
16
|
|
|
17
|
+
class _PermissiveSandboxedEnvironment(SandboxedEnvironment):
|
|
18
|
+
"""Custom Jinja2 sandbox that allows attribute access on trusted objects.
|
|
19
|
+
|
|
20
|
+
This environment allows attribute access on Pydantic models and other
|
|
21
|
+
trusted objects while still providing basic sandboxing protections.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def is_safe_attribute(self, obj: Any, attr: str, value: Any) -> bool:
|
|
25
|
+
"""Allow attribute access on Pydantic models and common types."""
|
|
26
|
+
# Allow attribute access on Pydantic models
|
|
27
|
+
if isinstance(obj, BaseModel):
|
|
28
|
+
return True
|
|
29
|
+
# Allow attribute access on dicts, lists, and other safe types
|
|
30
|
+
if isinstance(obj, (dict, list, tuple, str, int, float, bool, type(None))):
|
|
31
|
+
return True
|
|
32
|
+
# Default to parent's behavior for other types
|
|
33
|
+
return super().is_safe_attribute(obj, attr, value)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _permissive_jinja2_formatter(template: str, **kwargs: Any) -> str:
|
|
37
|
+
"""Format a template using a permissive Jinja2 sandbox.
|
|
38
|
+
|
|
39
|
+
Unlike LangChain's default restricted sandbox, this allows attribute
|
|
40
|
+
access on trusted objects like Pydantic models.
|
|
41
|
+
"""
|
|
42
|
+
env = _PermissiveSandboxedEnvironment()
|
|
43
|
+
return env.from_string(template).render(**kwargs)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# Override LangChain's default jinja2 formatter with our permissive one
|
|
47
|
+
# This allows nested attribute access on Pydantic models in templates
|
|
48
|
+
DEFAULT_FORMATTER_MAPPING["jinja2"] = _permissive_jinja2_formatter
|
|
49
|
+
|
|
50
|
+
|
|
16
51
|
class KeyTemplateRenderer(ChatPromptTemplate):
|
|
17
52
|
"""Derives ChatPromptTemplate and automatically provides template keys via
|
|
18
53
|
TemplateKeyProvider. This is to force standarization across types and keys."""
|
|
@@ -130,11 +130,10 @@ class FixCodeCommand(ChatCommand):
|
|
|
130
130
|
)
|
|
131
131
|
# Handle kwargs
|
|
132
132
|
source_file: SourceFile = SourceFile.load(
|
|
133
|
-
self.global_config.solution.filenames[0]
|
|
134
|
-
Path(os.getcwd()),
|
|
133
|
+
self.global_config.solution.filenames[0]
|
|
135
134
|
)
|
|
136
135
|
self.original_source_file = SourceFile(
|
|
137
|
-
source_file.file_path, source_file.
|
|
136
|
+
file_path=source_file.file_path, content=source_file.content
|
|
138
137
|
)
|
|
139
138
|
self.anim = (
|
|
140
139
|
LoadingWidget() if self.global_config.loading_hints else BaseLoadingWidget()
|
|
@@ -62,7 +62,7 @@ class CategoryFileHandler(logging.Handler):
|
|
|
62
62
|
if isinstance(category, Enum):
|
|
63
63
|
category = category.value
|
|
64
64
|
|
|
65
|
-
assert isinstance(category, str)
|
|
65
|
+
assert isinstance(category, str), f"Category is not string: {category}"
|
|
66
66
|
|
|
67
67
|
# Skip uncategorized if desired
|
|
68
68
|
if (
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
"""Keeps track of all the source files that ESBMC-AI is targeting."""
|
|
4
4
|
|
|
5
|
-
from os import
|
|
5
|
+
from os import walk
|
|
6
|
+
from os.path import commonpath
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from subprocess import PIPE, STDOUT, run, CompletedProcess
|
|
8
9
|
from tempfile import NamedTemporaryFile, TemporaryDirectory
|
|
@@ -13,8 +14,9 @@ from langchain_core.language_models import BaseChatModel
|
|
|
13
14
|
from langchain_core.load.serializable import Serializable
|
|
14
15
|
from pydantic import Field, PrivateAttr
|
|
15
16
|
import lizard
|
|
17
|
+
from structlog.stdlib import get_logger
|
|
16
18
|
|
|
17
|
-
from esbmc_ai.log_utils import get_log_level, print_horizontal_line
|
|
19
|
+
from esbmc_ai.log_utils import LogCategories, get_log_level, print_horizontal_line
|
|
18
20
|
|
|
19
21
|
_SourceFileFormatStyles = Literal["markdown", "xml", "plain"]
|
|
20
22
|
|
|
@@ -46,8 +48,7 @@ class SourceFile(Serializable):
|
|
|
46
48
|
"""Represents a source file in the Solution. Contains methods to manipulate
|
|
47
49
|
and get information about different versions."""
|
|
48
50
|
|
|
49
|
-
file_path: Path = Field(description="
|
|
50
|
-
base_path: Path = Field(description="Base directory path")
|
|
51
|
+
file_path: Path = Field(description="Absolute file path")
|
|
51
52
|
content: str = Field(description="File content")
|
|
52
53
|
|
|
53
54
|
@staticmethod
|
|
@@ -71,28 +72,23 @@ class SourceFile(Serializable):
|
|
|
71
72
|
return "\n".join(lines)
|
|
72
73
|
|
|
73
74
|
@staticmethod
|
|
74
|
-
def load(file_path: Path
|
|
75
|
+
def load(file_path: Path) -> "SourceFile":
|
|
75
76
|
"""Creates a new source file by loading the content from disk."""
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return SourceFile(file_path, base_path, file.read())
|
|
77
|
+
abs_path = file_path.absolute()
|
|
78
|
+
with open(abs_path, "r") as file:
|
|
79
|
+
return SourceFile(file_path=abs_path, content=file.read())
|
|
80
80
|
|
|
81
81
|
def __init__(
|
|
82
82
|
self,
|
|
83
83
|
file_path: Path,
|
|
84
|
-
base_path: Path,
|
|
85
84
|
content: str,
|
|
86
85
|
**kwargs: Any,
|
|
87
86
|
) -> None:
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
f"Use SourceFile.load() to create from absolute paths."
|
|
92
|
-
)
|
|
87
|
+
# Ensure path is absolute
|
|
88
|
+
if not file_path.is_absolute():
|
|
89
|
+
file_path = file_path.absolute()
|
|
93
90
|
super().__init__(
|
|
94
91
|
file_path=file_path,
|
|
95
|
-
base_path=base_path,
|
|
96
92
|
content=content,
|
|
97
93
|
**kwargs,
|
|
98
94
|
)
|
|
@@ -105,11 +101,6 @@ class SourceFile(Serializable):
|
|
|
105
101
|
def __str__(self) -> str:
|
|
106
102
|
return f"SourceFile({self.file_path})"
|
|
107
103
|
|
|
108
|
-
@property
|
|
109
|
-
def abs_path(self) -> Path:
|
|
110
|
-
"""Returns the abs path"""
|
|
111
|
-
return self.base_path / self.file_path
|
|
112
|
-
|
|
113
104
|
@property
|
|
114
105
|
def file_extension(self) -> str:
|
|
115
106
|
"""Returns the file extension to the file."""
|
|
@@ -199,10 +190,10 @@ class SourceFile(Serializable):
|
|
|
199
190
|
def verify_file_integrity(self) -> bool:
|
|
200
191
|
"""Verifies that this file matches with the file on disk and does not
|
|
201
192
|
contain any differences."""
|
|
202
|
-
if not (self.
|
|
193
|
+
if not (self.file_path.exists() and self.file_path.is_file()):
|
|
203
194
|
return False
|
|
204
195
|
|
|
205
|
-
with open(self.
|
|
196
|
+
with open(self.file_path, "r") as file:
|
|
206
197
|
content: str = str(file.read())
|
|
207
198
|
|
|
208
199
|
return content == self.content
|
|
@@ -233,6 +224,7 @@ class SourceFile(Serializable):
|
|
|
233
224
|
style: _SourceFileFormatStyles = "markdown",
|
|
234
225
|
include_line_numbers: bool = False,
|
|
235
226
|
max_lines: int | None = None,
|
|
227
|
+
working_dir: Path | None = None,
|
|
236
228
|
) -> str:
|
|
237
229
|
"""Flexible formatting with options for use in LangChain prompts.
|
|
238
230
|
|
|
@@ -262,12 +254,15 @@ class SourceFile(Serializable):
|
|
|
262
254
|
content = "\n".join(f"{i+1:4d} | {line}" for i, line in enumerate(lines))
|
|
263
255
|
|
|
264
256
|
# Format based on style
|
|
257
|
+
show_path: Path = (
|
|
258
|
+
self.file_path.relative_to(working_dir) if working_dir else self.file_path
|
|
259
|
+
)
|
|
265
260
|
if style == "markdown":
|
|
266
|
-
result = f"{
|
|
261
|
+
result = f"{show_path}\n```{lang}\n{content}\n```"
|
|
267
262
|
elif style == "xml":
|
|
268
|
-
result = f"<file path='{
|
|
263
|
+
result = f"<file path='{show_path}'>\n{content}\n</file>"
|
|
269
264
|
elif style == "plain":
|
|
270
|
-
result = f"File: {
|
|
265
|
+
result = f"File: {show_path}\n{content}"
|
|
271
266
|
|
|
272
267
|
return result
|
|
273
268
|
|
|
@@ -276,84 +271,96 @@ class Solution(Serializable):
|
|
|
276
271
|
"""Represents a solution, that is a collection of all the source files that
|
|
277
272
|
ESBMC-AI will be involved in analyzing."""
|
|
278
273
|
|
|
279
|
-
base_dir: Path = Field(description="Base directory path")
|
|
280
274
|
_files: list[SourceFile] = PrivateAttr(default_factory=list)
|
|
281
|
-
_include_dirs:
|
|
275
|
+
_include_dirs: list[Path] = PrivateAttr(default_factory=list)
|
|
282
276
|
|
|
283
277
|
@staticmethod
|
|
284
|
-
def
|
|
285
|
-
|
|
286
|
-
file_paths: list[Path] | None = None,
|
|
278
|
+
def from_paths(
|
|
279
|
+
*paths: Path,
|
|
287
280
|
include_dirs: list[Path] | None = None,
|
|
288
281
|
) -> "Solution":
|
|
289
|
-
"""Creates a solution from
|
|
290
|
-
path = path.absolute()
|
|
282
|
+
"""Creates a solution from one or more paths (files or directories).
|
|
291
283
|
|
|
292
|
-
|
|
293
|
-
return Solution(files=file_paths, base_dir=path, include_dirs=include_dirs)
|
|
284
|
+
Directories are scanned recursively for all files.
|
|
294
285
|
|
|
286
|
+
Args:
|
|
287
|
+
*paths: One or more file or directory paths
|
|
288
|
+
include_dirs: Optional include directories for C/C++ compilation
|
|
289
|
+
|
|
290
|
+
Example:
|
|
291
|
+
Solution.from_paths(Path("file1.c"), Path("file2.c"))
|
|
292
|
+
Solution.from_paths(Path("/project"))
|
|
293
|
+
Solution.from_paths(Path("src"), Path("tests/test.c"))
|
|
294
|
+
"""
|
|
295
295
|
result: list[Path] = []
|
|
296
|
-
for dir_path, _, files in walk(path):
|
|
297
|
-
result.extend(Path(dir_path) / file for file in files)
|
|
298
296
|
|
|
299
|
-
|
|
297
|
+
for path in paths:
|
|
298
|
+
abs_path = path.absolute()
|
|
299
|
+
if abs_path.is_file():
|
|
300
|
+
result.append(abs_path)
|
|
301
|
+
elif abs_path.is_dir():
|
|
302
|
+
for dir_path, _, files in walk(abs_path):
|
|
303
|
+
result.extend(Path(dir_path) / file for file in files)
|
|
304
|
+
else:
|
|
305
|
+
raise ValueError(
|
|
306
|
+
f"Path does not exist or is neither file nor directory: {path}"
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
return Solution(files=result, include_dirs=include_dirs)
|
|
300
310
|
|
|
301
311
|
def __init__(
|
|
302
312
|
self,
|
|
303
313
|
files: list[Path] | None = None,
|
|
304
|
-
base_dir: Path = Path(getcwd()),
|
|
305
314
|
include_dirs: list[Path] | None = None,
|
|
306
315
|
**kwargs: Any,
|
|
307
316
|
) -> None:
|
|
308
|
-
"""Creates a new solution
|
|
309
|
-
super().__init__(
|
|
317
|
+
"""Creates a new solution from file paths."""
|
|
318
|
+
super().__init__(**kwargs)
|
|
310
319
|
|
|
311
320
|
# Initialize private attributes
|
|
312
321
|
self._files = []
|
|
313
|
-
self._include_dirs =
|
|
322
|
+
self._include_dirs = []
|
|
314
323
|
|
|
315
324
|
# If files are loaded
|
|
316
325
|
if files:
|
|
317
326
|
for file_path in files:
|
|
318
|
-
#
|
|
319
|
-
|
|
320
|
-
file_path
|
|
321
|
-
if file_path.is_absolute()
|
|
322
|
-
else (self.base_dir / file_path)
|
|
327
|
+
# Convert to absolute path
|
|
328
|
+
abs_path = (
|
|
329
|
+
file_path if file_path.is_absolute() else file_path.absolute()
|
|
323
330
|
)
|
|
324
|
-
if not
|
|
331
|
+
if not abs_path.is_file():
|
|
325
332
|
raise ValueError(f"Path is not a file: {file_path}")
|
|
326
|
-
|
|
327
|
-
# normalize absolute paths to relative ones here
|
|
328
|
-
if file_path.is_absolute():
|
|
329
|
-
if file_path.is_relative_to(self.base_dir):
|
|
330
|
-
file_path = file_path.relative_to(self.base_dir)
|
|
331
|
-
else:
|
|
332
|
-
raise ValueError(
|
|
333
|
-
f"File path '{file_path}' is not within base directory '{self.base_dir}'"
|
|
334
|
-
)
|
|
335
|
-
self.load_source_file(file_path)
|
|
333
|
+
self._files.append(SourceFile.load(abs_path))
|
|
336
334
|
|
|
337
335
|
if include_dirs:
|
|
338
336
|
for d in include_dirs:
|
|
339
|
-
if
|
|
337
|
+
abs_dir = d if d.is_absolute() else d.absolute()
|
|
338
|
+
if not abs_dir.is_dir():
|
|
340
339
|
raise ValueError(f"Path is not a directory: {d}")
|
|
341
|
-
|
|
342
|
-
self._include_dirs[d] = [
|
|
343
|
-
SourceFile.load(file_path=p, base_path=self.base_dir)
|
|
344
|
-
for p in include_files
|
|
345
|
-
]
|
|
340
|
+
self._include_dirs.append(abs_dir)
|
|
346
341
|
|
|
347
342
|
@property
|
|
348
|
-
def include_dirs(self) ->
|
|
349
|
-
"""
|
|
350
|
-
return self._include_dirs
|
|
343
|
+
def include_dirs(self) -> list[Path]:
|
|
344
|
+
"""Include directories for C based verifiers."""
|
|
345
|
+
return list(self._include_dirs)
|
|
351
346
|
|
|
352
347
|
@property
|
|
353
348
|
def files(self) -> list[SourceFile]:
|
|
354
349
|
"""Will return a list of the files. Returns by value."""
|
|
355
350
|
return list(self._files)
|
|
356
351
|
|
|
352
|
+
@property
|
|
353
|
+
def working_dir(self) -> Path:
|
|
354
|
+
"""Computes the common parent directory of all files."""
|
|
355
|
+
if not self._files:
|
|
356
|
+
return Path.cwd()
|
|
357
|
+
|
|
358
|
+
paths = [str(f.file_path) for f in self._files]
|
|
359
|
+
common = commonpath(paths)
|
|
360
|
+
|
|
361
|
+
# If only one file, commonpath returns the file itself, so use parent
|
|
362
|
+
return Path(common).parent if len(paths) == 1 else Path(common)
|
|
363
|
+
|
|
357
364
|
def get_files_by_ext(self, included_ext: list[str]) -> list[SourceFile]:
|
|
358
365
|
"""Gets the files that are only specified in the included extensions. File
|
|
359
366
|
extensions that have a . prefix are trimmed so they still work."""
|
|
@@ -401,43 +408,48 @@ class Solution(Serializable):
|
|
|
401
408
|
"""Saves the solution in a temporary directory it creates while preserving
|
|
402
409
|
the file structure."""
|
|
403
410
|
with TemporaryDirectory(
|
|
404
|
-
prefix="esbmc-ai", suffix=self.
|
|
411
|
+
prefix="esbmc-ai-", suffix="-" + self.working_dir.name, delete=False
|
|
405
412
|
) as temp_dir:
|
|
406
413
|
return self.save_solution(Path(temp_dir))
|
|
407
414
|
|
|
408
415
|
def save_solution(self, path: Path) -> "Solution":
|
|
409
|
-
"""Saves the solution to path,
|
|
410
|
-
|
|
411
|
-
|
|
416
|
+
"""Saves the solution to path, preserving relative structure.
|
|
417
|
+
|
|
418
|
+
Files are saved relative to their common parent (working_dir)."""
|
|
419
|
+
get_logger().bind(category=LogCategories.SYSTEM).info(
|
|
420
|
+
f"Saving solution to {path}"
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
dest_path: Path = path.absolute()
|
|
424
|
+
dest_path.mkdir(parents=True, exist_ok=True)
|
|
425
|
+
|
|
426
|
+
# Get common parent to preserve relative structure
|
|
427
|
+
common_parent = self.working_dir
|
|
412
428
|
|
|
413
|
-
# Copy individual source files
|
|
429
|
+
# Copy individual source files preserving structure
|
|
414
430
|
new_file_paths: list[Path] = []
|
|
415
431
|
for source_file in self.files:
|
|
416
|
-
#
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
else:
|
|
420
|
-
relative_path = source_file.file_path
|
|
432
|
+
# Get path relative to common parent
|
|
433
|
+
relative_path = source_file.file_path.relative_to(common_parent)
|
|
434
|
+
new_path: Path = dest_path / relative_path
|
|
421
435
|
|
|
422
|
-
new_file_paths.append(relative_path)
|
|
423
|
-
new_path: Path = base_dir_path / relative_path
|
|
424
436
|
# Write new file
|
|
425
437
|
new_path.parent.mkdir(parents=True, exist_ok=True)
|
|
426
438
|
source_file.save_file(new_path)
|
|
439
|
+
new_file_paths.append(new_path)
|
|
427
440
|
|
|
428
|
-
# Copy include directories
|
|
441
|
+
# Copy include directories
|
|
442
|
+
new_include_dirs: list[Path] = []
|
|
429
443
|
for d in self.include_dirs:
|
|
430
|
-
new_dir: Path =
|
|
444
|
+
new_dir: Path = dest_path / d.name
|
|
431
445
|
copytree(src=d, dst=new_dir, symlinks=True, dirs_exist_ok=True)
|
|
446
|
+
new_include_dirs.append(new_dir)
|
|
432
447
|
|
|
433
|
-
|
|
448
|
+
return Solution(
|
|
434
449
|
files=new_file_paths,
|
|
435
|
-
|
|
436
|
-
include_dirs=list(self.include_dirs.keys()),
|
|
450
|
+
include_dirs=new_include_dirs,
|
|
437
451
|
)
|
|
438
452
|
|
|
439
|
-
return sol
|
|
440
|
-
|
|
441
453
|
def verify_solution_integrity(self) -> bool:
|
|
442
454
|
"""Verifies if the content of the solution match with the files on disk.
|
|
443
455
|
If that's not the case, then the solution should be saved on disk before
|
|
@@ -467,23 +479,9 @@ class Solution(Serializable):
|
|
|
467
479
|
self.load_source_file(f)
|
|
468
480
|
|
|
469
481
|
def load_source_file(self, file_path: Path) -> None:
|
|
470
|
-
"""Add a source file to the solution
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
if file_path.is_absolute():
|
|
474
|
-
raise ValueError(
|
|
475
|
-
f"Cannot load absolute path '{file_path}' into Solution. "
|
|
476
|
-
f"All file paths must be relative to the base directory '{self.base_dir}'. "
|
|
477
|
-
f"This is an internal error - paths should be normalized before reaching this point."
|
|
478
|
-
)
|
|
479
|
-
with open(self.base_dir / file_path, "r") as file:
|
|
480
|
-
self._files.append(
|
|
481
|
-
SourceFile(
|
|
482
|
-
file_path=file_path,
|
|
483
|
-
base_path=self.base_dir,
|
|
484
|
-
content=file.read(),
|
|
485
|
-
)
|
|
486
|
-
)
|
|
482
|
+
"""Add a source file to the solution by loading it from disk."""
|
|
483
|
+
abs_path = file_path if file_path.is_absolute() else file_path.absolute()
|
|
484
|
+
self._files.append(SourceFile.load(abs_path))
|
|
487
485
|
|
|
488
486
|
def get_diff(self, other: "Solution") -> str:
|
|
489
487
|
"""Gets the diff with another solution. The following params are used: -ruN"""
|
|
@@ -495,10 +493,10 @@ class Solution(Serializable):
|
|
|
495
493
|
other.save_temp()
|
|
496
494
|
|
|
497
495
|
cmd = [
|
|
498
|
-
"
|
|
496
|
+
"diff",
|
|
499
497
|
"-ruN", # (r)ecursive, (u) additional context, (N) treat absent files as empty
|
|
500
|
-
self.
|
|
501
|
-
other.
|
|
498
|
+
str(self.working_dir),
|
|
499
|
+
str(other.working_dir),
|
|
502
500
|
]
|
|
503
501
|
|
|
504
502
|
process: CompletedProcess = run(
|
|
@@ -512,11 +510,18 @@ class Solution(Serializable):
|
|
|
512
510
|
# https://askubuntu.com/questions/698784/exit-code-of-diff
|
|
513
511
|
if process.returncode == 2:
|
|
514
512
|
raise ValueError(
|
|
515
|
-
f"Diff for {self.
|
|
513
|
+
f"Diff for {self.working_dir} and {other.working_dir} failed (exit 2)."
|
|
516
514
|
)
|
|
517
515
|
|
|
518
516
|
return process.stdout.decode("utf-8")
|
|
519
517
|
|
|
518
|
+
def save_diff(self, path: Path, solution: "Solution") -> Path:
|
|
519
|
+
"""Saves the solution as a patch of the original."""
|
|
520
|
+
diff: str = self.get_diff(solution)
|
|
521
|
+
with open(path, "w") as file:
|
|
522
|
+
file.write(diff)
|
|
523
|
+
return path
|
|
524
|
+
|
|
520
525
|
def resolve(self, file: SourceFile | Path) -> SourceFile | None:
|
|
521
526
|
"""Resolve a Path or SourceFile to its corresponding SourceFile in the solution.
|
|
522
527
|
|
|
@@ -527,23 +532,14 @@ class Solution(Serializable):
|
|
|
527
532
|
The SourceFile if found, None otherwise
|
|
528
533
|
"""
|
|
529
534
|
if isinstance(file, SourceFile):
|
|
530
|
-
|
|
531
|
-
for f in self._files:
|
|
532
|
-
if f.abs_path == file.abs_path:
|
|
533
|
-
return f
|
|
534
|
-
return None
|
|
535
|
-
|
|
536
|
-
# For Path, handle both absolute and relative
|
|
537
|
-
path = Path(file)
|
|
538
|
-
if path.is_absolute():
|
|
539
|
-
for f in self._files:
|
|
540
|
-
if f.abs_path == path:
|
|
541
|
-
return f
|
|
535
|
+
search_path = file.file_path
|
|
542
536
|
else:
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
return f
|
|
537
|
+
# Convert to absolute path for comparison
|
|
538
|
+
search_path = Path(file).absolute()
|
|
546
539
|
|
|
540
|
+
for f in self._files:
|
|
541
|
+
if f.file_path == search_path:
|
|
542
|
+
return f
|
|
547
543
|
return None
|
|
548
544
|
|
|
549
545
|
def __contains__(self, file: SourceFile | Path) -> bool:
|
|
@@ -574,6 +570,28 @@ class Solution(Serializable):
|
|
|
574
570
|
raise KeyError(f"Path not found in solution: {path}")
|
|
575
571
|
return result
|
|
576
572
|
|
|
573
|
+
def __add__(self, other: "Solution") -> "Solution":
|
|
574
|
+
"""Combine two solutions by merging their files and include directories.
|
|
575
|
+
|
|
576
|
+
Args:
|
|
577
|
+
other: Another Solution instance to combine with this one
|
|
578
|
+
|
|
579
|
+
Returns:
|
|
580
|
+
A new Solution containing all files and include_dirs from both solutions
|
|
581
|
+
|
|
582
|
+
Example:
|
|
583
|
+
solution_c = solution_a + solution_b
|
|
584
|
+
"""
|
|
585
|
+
if not isinstance(other, Solution):
|
|
586
|
+
return NotImplemented
|
|
587
|
+
|
|
588
|
+
return Solution(
|
|
589
|
+
# Combine files - create new list to avoid modifying originals
|
|
590
|
+
files=[source_file.file_path for source_file in self._files + other._files],
|
|
591
|
+
# Combine include_dirs - deduplicate by converting to set and back
|
|
592
|
+
include_dirs=list(set(self._include_dirs + other._include_dirs)),
|
|
593
|
+
)
|
|
594
|
+
|
|
577
595
|
def patch_solution(self, patch: str) -> None:
|
|
578
596
|
"""Patches the solution using the patch command."""
|
|
579
597
|
if not self.verify_solution_integrity():
|
|
@@ -586,10 +604,10 @@ class Solution(Serializable):
|
|
|
586
604
|
|
|
587
605
|
cmd = [
|
|
588
606
|
"patch",
|
|
589
|
-
"-d", # Change to
|
|
590
|
-
self.
|
|
607
|
+
"-d", # Change to working dir first
|
|
608
|
+
str(self.working_dir),
|
|
591
609
|
"-i", # Flag to specify patch file
|
|
592
|
-
Path(patch_file.name).absolute(),
|
|
610
|
+
str(Path(patch_file.name).absolute()),
|
|
593
611
|
]
|
|
594
612
|
|
|
595
613
|
process: CompletedProcess = run(
|
|
@@ -610,7 +628,7 @@ class Solution(Serializable):
|
|
|
610
628
|
+ process.stdout.decode("utf-8")
|
|
611
629
|
)
|
|
612
630
|
raise ValueError(
|
|
613
|
-
f"Patch failed for some files in solution {self.
|
|
631
|
+
f"Patch failed for some files in solution {self.working_dir}"
|
|
614
632
|
)
|
|
615
633
|
case 2:
|
|
616
634
|
print_horizontal_line(get_log_level(0))
|
|
@@ -619,5 +637,5 @@ class Solution(Serializable):
|
|
|
619
637
|
+ process.stdout.decode("utf-8")
|
|
620
638
|
)
|
|
621
639
|
raise ValueError(
|
|
622
|
-
f"Patch failed SERIOUSLY in solution {self.
|
|
640
|
+
f"Patch failed SERIOUSLY in solution {self.working_dir}"
|
|
623
641
|
)
|
|
@@ -151,7 +151,7 @@ class ESBMCOutputParser:
|
|
|
151
151
|
@staticmethod
|
|
152
152
|
def _should_include_trace(trace: ProgramTrace, solution: Solution) -> bool:
|
|
153
153
|
"""Determine if a trace should be included based on solution files."""
|
|
154
|
-
return (solution.
|
|
154
|
+
return (solution.working_dir / trace.path).exists() and trace.path in solution
|
|
155
155
|
|
|
156
156
|
@staticmethod
|
|
157
157
|
def filter_traces(esbmc_output: "ESBMCOutput", solution: Solution) -> "ESBMCOutput":
|
|
@@ -668,7 +668,7 @@ class ESBMC(BaseSourceVerifier):
|
|
|
668
668
|
str(file.file_path) for file in solution.get_files_by_ext(["c", "cpp"])
|
|
669
669
|
)
|
|
670
670
|
# Header files/dir
|
|
671
|
-
esbmc_cmd.extend("-I" + str(d) for d in solution.include_dirs
|
|
671
|
+
esbmc_cmd.extend("-I" + str(d) for d in solution.include_dirs)
|
|
672
672
|
|
|
673
673
|
# Add timeout suffix for parameter.
|
|
674
674
|
if timeout:
|
|
@@ -686,12 +686,12 @@ class ESBMC(BaseSourceVerifier):
|
|
|
686
686
|
# Measure execution time
|
|
687
687
|
start_time = perf_counter()
|
|
688
688
|
|
|
689
|
-
# Run ESBMC from solution
|
|
689
|
+
# Run ESBMC from solution working_dir and get output
|
|
690
690
|
process: CompletedProcess = run(
|
|
691
691
|
esbmc_cmd,
|
|
692
692
|
stdout=PIPE,
|
|
693
693
|
stderr=STDOUT,
|
|
694
|
-
cwd=solution.
|
|
694
|
+
cwd=solution.working_dir,
|
|
695
695
|
timeout=process_timeout,
|
|
696
696
|
check=False,
|
|
697
697
|
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|