Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 34 additions & 15 deletions openevolve/utils/code_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,24 +206,43 @@ def extract_diffs(
ValueError: The standard SEARCH/REPLACE response has an extra or
unmatched delimiter line. Custom diff patterns retain their own grammar.
"""
matches = list(re.finditer(diff_pattern, diff_text, re.DOTALL))
if diff_pattern == _STANDARD_DIFF_PATTERN:
# A permissive regex can absorb a second separator into replacement
# text and then insert it as code. Validate the complete response's
# delimiter lines before applying any matched block.
cursor = 0
for match in matches:
if _STANDARD_DIFF_MARKER.search(diff_text[cursor : match.start()]):
raise ValueError("Unmatched SEARCH/REPLACE delimiter outside a diff block")
block_markers = [
marker.group(1) for marker in _STANDARD_DIFF_MARKER.finditer(match.group(0))
]
if block_markers != ["<<<<<<< SEARCH", "=======", ">>>>>>> REPLACE"]:
return _extract_standard_diffs(diff_text)
matches = re.finditer(diff_pattern, diff_text, re.DOTALL)
return [(match.group(1).rstrip(), match.group(2).rstrip()) for match in matches]


def _extract_standard_diffs(diff_text: str) -> List[Tuple[str, str]]:
"""
Extract SEARCH/REPLACE blocks delimited by whole marker lines

A marker only counts on a line of its own, so code such as a "# =====" banner
comment stays part of a block. Every marker line in the response is checked
before any block is returned: a permissive match could otherwise absorb a
second separator into replacement text and insert it as code.
"""
markers = ["<<<<<<< SEARCH", "=======", ">>>>>>> REPLACE"]
blocks: List[Tuple[str, str]] = []
texts: List[str] = [] # SEARCH and REPLACE text of the block being read
expected = 0
section_start = 0
for marker in _STANDARD_DIFF_MARKER.finditer(diff_text):
token = marker.group(1)
if token != markers[expected]:
if token == markers[1] and expected == 2:
raise ValueError("Malformed SEARCH/REPLACE delimiter sequence")
cursor = match.end()
if _STANDARD_DIFF_MARKER.search(diff_text[cursor:]):
raise ValueError("Unmatched SEARCH/REPLACE delimiter outside a diff block")
return [(match.group(1).rstrip(), match.group(2).rstrip()) for match in matches]
if expected > 0:
texts.append(diff_text[section_start : marker.start()].rstrip())
# A section's text starts on the line after its marker
section_start = marker.end() + 1
expected = (expected + 1) % 3
if expected == 0:
blocks.append((texts[0], texts[1]))
texts = []
if expected:
raise ValueError("Unmatched SEARCH/REPLACE delimiter outside a diff block")
return blocks


def parse_full_rewrite(llm_response: str, language: str = "python") -> Optional[str]:
Expand Down
28 changes: 23 additions & 5 deletions tests/test_code_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,12 +237,14 @@ def test_separator_with_trailing_whitespace_raises(self):
with self.assertRaises(ValueError):
extract_diffs(text)

def test_crlf_marker_is_recognised(self):
# Standard pattern needs LF after markers, so a CRLF response yields no
# match; its marker lines are then reported rather than silently ignored.
def test_crlf_response_is_parsed(self):
text = "<<<<<<< SEARCH\r\nx = 1\r\n=======\r\nx = 2\r\n>>>>>>> REPLACE\r\n"
with self.assertRaises(ValueError):
extract_diffs(text)
self.assertEqual(extract_diffs(text), [("x = 1", "x = 2")])
self.assertEqual(apply_diff("x = 1\ny = 1", text), "x = 2\ny = 1")

def test_marker_lines_with_trailing_whitespace_are_parsed(self):
text = "<<<<<<< SEARCH \nx = 1\n=======\t\nx = 2\n>>>>>>> REPLACE \n"
self.assertEqual(extract_diffs(text), [("x = 1", "x = 2")])

def test_apply_diff_leaves_no_partial_changes(self):
# A valid first block must not be applied when a later block is malformed.
Expand All @@ -266,6 +268,22 @@ def test_separator_inside_code_line_is_allowed(self):
text = f"<<<<<<< SEARCH\nx = 1\n=======\n{replace}\n>>>>>>> REPLACE\n"
self.assertEqual(extract_diffs(text), [("x = 1", replace)])

def test_code_line_ending_in_separator_does_not_split_search(self):
# A banner comment such as "# =====" ends with the separator text; only a
# line that is the separator ends the SEARCH section.
banner = "# " + "=" * 25
search = f"{banner}\n# Helpers\n{banner}\ndef f(x):\n return x + 1"
replace = f"{banner}\n# Helpers\n{banner}\ndef f(x):\n return x + 2"
text = f"<<<<<<< SEARCH\n{search}\n=======\n{replace}\n>>>>>>> REPLACE\n"
self.assertEqual(extract_diffs(text), [(search, replace)])
self.assertEqual(apply_diff(search + "\n", text), replace + "\n")

def test_code_line_ending_in_marker_text_is_not_a_marker(self):
search = "x = 1 # <<<<<<< SEARCH"
replace = "x = 2 # >>>>>>> REPLACE"
text = f"<<<<<<< SEARCH\n{search}\n=======\n{replace}\n>>>>>>> REPLACE\n"
self.assertEqual(extract_diffs(text), [(search, replace)])

def test_marker_words_in_prose_are_allowed(self):
text = (
"I use the ======= separator and >>>>>>> REPLACE marker below.\n"
Expand Down
Loading