Skip to content

Commit e0bbf48

Browse files
committed
postprocess: include C definitions before and after preprocessing in prompts
Read both forms from the structured *.c_decls.json instead of running a clang subprocess per snippet. The CommentsTransform prompt includes the preprocessed text only when it differs from the original, so prompts (and llm-cache keys) are unchanged for directive-free functions. Comment gating and response validation use comments from the preprocessed text plus comments on original preprocessor directive lines, so inactive-region comments are not transferred while directive-line comments are not dropped just because clang omits them from directives-only output.
1 parent c82c03f commit e0bbf48

5 files changed

Lines changed: 175 additions & 20 deletions

File tree

c2rust-postprocess/postprocess/definitions/__init__.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import logging
33
import subprocess
44
from collections.abc import Generator, Iterable
5+
from dataclasses import dataclass
56
from pathlib import Path
67
from tempfile import NamedTemporaryFile
78
from typing import Any
@@ -233,12 +234,37 @@ def get_rust_definitions(root_rust_source_file: Path) -> dict[str, str]:
233234
return json.loads(result.stdout)
234235

235236

236-
def get_c_definitions(root_rust_source_file: Path) -> dict[str, str]:
237+
@dataclass(frozen=True)
238+
class CDefinition:
239+
definition: str
240+
preprocessed_definition: str | None
241+
242+
@property
243+
def effective(self) -> str:
244+
"""Preprocessed text when available, original otherwise."""
245+
return self.preprocessed_definition or self.definition
246+
247+
@property
248+
def was_changed_by_preprocessing(self) -> bool:
249+
return (
250+
self.preprocessed_definition is not None
251+
and self.preprocessed_definition != self.definition
252+
)
253+
254+
255+
def get_c_definitions(root_rust_source_file: Path) -> dict[str, CDefinition]:
237256
c_defs_json = root_rust_source_file.with_suffix(".c_decls.json")
238257

239258
logging.debug(f"Loading C definitions from {c_defs_json}")
240259
try:
241-
return json.loads(c_defs_json.read_text())
260+
c_decls = json.loads(c_defs_json.read_text())
261+
return {
262+
identifier: CDefinition(
263+
definition=decl["definition"],
264+
preprocessed_definition=decl.get("preprocessed_definition"),
265+
)
266+
for identifier, decl in c_decls["definitions"].items()
267+
}
242268
except OSError as e:
243269
e.add_note(f"C definitions JSON file not found: {c_defs_json}")
244270
raise e

c2rust-postprocess/postprocess/transforms/base.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from pathlib import Path
55

66
from postprocess.definitions import (
7+
CDefinition,
78
get_c_definitions,
89
get_rust_definitions,
910
)
@@ -32,7 +33,7 @@ def apply_ident(
3233
self,
3334
rust_source_file: Path,
3435
rust_definition: str,
35-
c_definition: str,
36+
c_definition: CDefinition,
3637
identifier: str,
3738
update_rust: bool = True,
3839
) -> None:
@@ -114,7 +115,7 @@ def apply_file(
114115

115116
c_definition = c_definitions[identifier]
116117

117-
highlighted_c_definition = get_highlighted_c(c_definition)
118+
highlighted_c_definition = get_highlighted_c(c_definition.effective)
118119
logging.debug(
119120
f"C function {identifier} definition:\n{highlighted_c_definition}\n"
120121
)

c2rust-postprocess/postprocess/transforms/comments.py

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import logging
2+
from collections import Counter
23
from pathlib import Path
34
from textwrap import dedent
45

56
from postprocess.cache import AbstractCache
67
from postprocess.definitions import (
8+
CDefinition,
79
get_c_comments,
810
get_rust_comments,
911
update_rust_definition,
@@ -20,31 +22,74 @@
2022

2123
class CommentsTransformPrompt:
2224
c_function: str
25+
c_function_preprocessed: str | None
2326
rust_function: str
2427
prompt_text: str
2528
identifier: str
2629

27-
__slots__ = ("c_function", "rust_function", "prompt_text", "identifier")
30+
__slots__ = (
31+
"c_function",
32+
"c_function_preprocessed",
33+
"rust_function",
34+
"prompt_text",
35+
"identifier",
36+
)
2837

2938
def __init__(
30-
self, c_function: str, rust_function: str, prompt_text: str, identifier: str
39+
self,
40+
c_function: str,
41+
c_function_preprocessed: str | None,
42+
rust_function: str,
43+
prompt_text: str,
44+
identifier: str,
3145
):
3246
self.c_function = c_function
47+
self.c_function_preprocessed = c_function_preprocessed
3348
self.rust_function = rust_function
3449
self.prompt_text = prompt_text
3550
self.identifier = identifier
3651

3752
def __str__(self) -> str:
38-
return (
53+
prompt = (
3954
self.prompt_text
4055
+ "\n\n"
4156
+ "C function:\n```c\n"
4257
+ self.c_function
4358
+ "```\n\n"
44-
+ "Rust function:\n```rust\n"
45-
+ self.rust_function
46-
+ "```\n"
4759
)
60+
if self.c_function_preprocessed is not None:
61+
prompt += (
62+
"The same C function after preprocessing; comments that are"
63+
" absent here may have been in inactive preprocessor regions"
64+
" and must not be transferred. Comments on preprocessor"
65+
" directive lines in the original C function may be absent"
66+
" here even when they annotate active code:\n```c\n"
67+
+ self.c_function_preprocessed
68+
+ "```\n\n"
69+
)
70+
prompt += "Rust function:\n```rust\n" + self.rust_function + "```\n"
71+
return prompt
72+
73+
74+
def _get_directive_line_comments(code: str) -> list[str]:
75+
return get_c_comments(
76+
"\n".join(line for line in code.splitlines() if line.lstrip().startswith("#"))
77+
)
78+
79+
80+
def _get_transferable_c_comments(c_definition: CDefinition) -> list[str]:
81+
if c_definition.preprocessed_definition is None:
82+
return get_c_comments(c_definition.definition)
83+
84+
wanted_comments = Counter(get_c_comments(c_definition.preprocessed_definition))
85+
wanted_comments.update(_get_directive_line_comments(c_definition.definition))
86+
87+
comments = []
88+
for comment in get_c_comments(c_definition.definition):
89+
if wanted_comments[comment] > 0:
90+
comments.append(comment)
91+
wanted_comments[comment] -= 1
92+
return comments
4893

4994

5095
class CommentsTransform(AbstractTransform):
@@ -57,7 +102,7 @@ def apply_ident(
57102
self,
58103
rust_source_file: Path,
59104
rust_definition: str,
60-
c_definition: str,
105+
c_definition: CDefinition,
61106
identifier: str,
62107
update_rust: bool = True,
63108
) -> None:
@@ -70,8 +115,7 @@ def apply_ident(
70115
)
71116
return
72117

73-
# Skip functions without comments in C definition
74-
c_comments = get_c_comments(c_definition)
118+
c_comments = _get_transferable_c_comments(c_definition)
75119
if not c_comments:
76120
logging.info(f"Skipping C function without comments: {identifier}")
77121
return
@@ -86,7 +130,14 @@ def apply_ident(
86130
prompt_text = dedent(prompt_text).strip()
87131

88132
prompt = CommentsTransformPrompt(
89-
c_function=c_definition,
133+
c_function=c_definition.definition,
134+
# Only include the preprocessed text when it differs, so prompts
135+
# (and thus cache keys) are unchanged for directive-free functions.
136+
c_function_preprocessed=(
137+
c_definition.preprocessed_definition
138+
if c_definition.was_changed_by_preprocessing
139+
else None
140+
),
90141
rust_function=rust_definition,
91142
prompt_text=prompt_text,
92143
identifier=identifier,
@@ -121,7 +172,6 @@ def apply_ident(
121172

122173
rust_fn = remove_backticks(response)
123174

124-
c_comments = get_c_comments(prompt.c_function)
125175
logging.debug(f"{c_comments=}")
126176

127177
rust_comments = get_rust_comments(rust_fn)
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1 @@
1-
{
2-
"swap": "void swap(int* a, int* b)\n{\n int t = *a;\n *a = *b;\n *b = t;\n}",
3-
"quickSort": "void quickSort(int arr[], int low, int high)\n{\n if (low < high) {\n /* pi is the partitioning index; arr[pi] is now at the right place */\n int pi = partition(arr, low, high);\n\n /* Recursively sort elements before and after partition */\n quickSort(arr, low, pi - 1);\n quickSort(arr, pi + 1, high);\n }\n}",
4-
"partition": "/* \n * Lomuto Partition Scheme:\n * Partitions the array so that elements < pivot are on the left, \n * and elements >= pivot are on the right.\n */\nint partition (int arr[], int low, int high)\n{\n // Partition the subarray around the last element as pivot and return pivot's final index.\n int pivot = arr[high];\n int i = low - 1;\n\n for (int j = low; j <= high - 1; j++) {\n if (arr[j] <= pivot) {\n i++;\n // Move elements <= pivot into the left partition.\n swap(&arr[i], &arr[j]);\n }\n }\n // Place pivot just after the final element of the left partition.\n swap(&arr[i + 1], &arr[high]);\n return i + 1;\n}"
5-
}
1+
{"definitions":{"swap":{"definition":"void swap(int* a, int* b)\n{\n int t = *a;\n *a = *b;\n *b = t;\n}","preprocessed_definition":"void swap(int* a, int* b)\n{\n int t = *a;\n *a = *b;\n *b = t;\n}"},"partition":{"definition":"/* \n * Lomuto Partition Scheme:\n * Partitions the array so that elements < pivot are on the left, \n * and elements >= pivot are on the right.\n */\nint partition (int arr[], int low, int high)\n{\n // Partition the subarray around the last element as pivot and return pivot's final index.\n int pivot = arr[high];\n int i = low - 1;\n\n for (int j = low; j <= high - 1; j++) {\n if (arr[j] <= pivot) {\n i++;\n // Move elements <= pivot into the left partition.\n swap(&arr[i], &arr[j]);\n }\n }\n // Place pivot just after the final element of the left partition.\n swap(&arr[i + 1], &arr[high]);\n return i + 1;\n}","preprocessed_definition":"/* \n * Lomuto Partition Scheme:\n * Partitions the array so that elements < pivot are on the left, \n * and elements >= pivot are on the right.\n */\nint partition (int arr[], int low, int high)\n{\n // Partition the subarray around the last element as pivot and return pivot's final index.\n int pivot = arr[high];\n int i = low - 1;\n\n for (int j = low; j <= high - 1; j++) {\n if (arr[j] <= pivot) {\n i++;\n // Move elements <= pivot into the left partition.\n swap(&arr[i], &arr[j]);\n }\n }\n // Place pivot just after the final element of the left partition.\n swap(&arr[i + 1], &arr[high]);\n return i + 1;\n}"},"quickSort":{"definition":"void quickSort(int arr[], int low, int high)\n{\n if (low < high) {\n /* pi is the partitioning index; arr[pi] is now at the right place */\n int pi = partition(arr, low, high);\n\n /* Recursively sort elements before and after partition */\n quickSort(arr, low, pi - 1);\n quickSort(arr, pi + 1, high);\n }\n}","preprocessed_definition":"void quickSort(int arr[], int low, int high)\n{\n if (low < high) {\n /* pi is the partitioning index; arr[pi] is now at the right place */\n int pi = partition(arr, low, high);\n\n /* Recursively sort elements before and after partition */\n quickSort(arr, low, pi - 1);\n quickSort(arr, pi + 1, high);\n }\n}"}}}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from pathlib import Path
2+
from typing import Any
3+
4+
from postprocess.cache import AbstractCache
5+
from postprocess.definitions import CDefinition
6+
from postprocess.models.mock import MockGenerativeModel
7+
from postprocess.transforms.comments import CommentsTransform
8+
9+
10+
class StaticCache(AbstractCache):
11+
def __init__(self, response: str):
12+
super().__init__(Path())
13+
self.response = response
14+
self.lookups = 0
15+
self.messages: list[list[dict[str, Any]]] = []
16+
17+
def lookup(
18+
self,
19+
*,
20+
transform: str,
21+
identifier: str,
22+
model: str,
23+
messages: list[dict[str, Any]],
24+
) -> str | None:
25+
self.lookups += 1
26+
self.messages.append(messages)
27+
return self.response
28+
29+
def update(
30+
self,
31+
*,
32+
transform: str,
33+
identifier: str,
34+
model: str,
35+
messages: list[dict[str, Any]],
36+
response: str,
37+
) -> None:
38+
raise AssertionError("cached response should not be updated")
39+
40+
41+
def test_directive_line_comment_survives_preprocessed_check() -> None:
42+
c_definition = CDefinition(
43+
definition="""\
44+
int enabled(void) {
45+
#ifdef FEATURE
46+
return 1;
47+
#endif /* enabled path */
48+
}
49+
""",
50+
preprocessed_definition="""\
51+
int enabled(void) {
52+
53+
return 1;
54+
55+
}
56+
""",
57+
)
58+
rust_definition = """\
59+
pub unsafe extern "C" fn enabled() -> libc::c_int {
60+
return 1 as libc::c_int;
61+
}
62+
"""
63+
response = """\
64+
/// enabled path
65+
pub unsafe extern "C" fn enabled() -> libc::c_int {
66+
return 1 as libc::c_int;
67+
}
68+
"""
69+
cache = StaticCache(response)
70+
transform = CommentsTransform(cache=cache, model=MockGenerativeModel())
71+
72+
transform.apply_ident(
73+
rust_source_file=Path("unused.rs"),
74+
rust_definition=rust_definition,
75+
c_definition=c_definition,
76+
identifier="enabled",
77+
update_rust=False,
78+
)
79+
80+
assert cache.lookups == 1
81+
prompt = cache.messages[0][0]["content"]
82+
assert "preprocessor directive lines" in prompt

0 commit comments

Comments
 (0)