Skip to content

Commit 962e684

Browse files
committed
postprocess: add transformation to compact verbose Rust formatting
1 parent a8f33e8 commit 962e684

3 files changed

Lines changed: 226 additions & 1 deletion

File tree

‎c2rust-postprocess/postprocess/__init__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from postprocess.utils import existing_file
2626

2727
DEFAULT_LLM_MODEL = "gemini-3.5-flash"
28-
DEFAULT_TRANSFORMS = ("comments", "asserts")
28+
DEFAULT_TRANSFORMS = ("comments", "asserts", "formatting")
2929

3030

3131
def build_arg_parser() -> argparse.ArgumentParser:

‎c2rust-postprocess/postprocess/transforms/__init__.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from postprocess.transforms.asserts import AssertsTransform
44
from postprocess.transforms.base import AbstractTransform
55
from postprocess.transforms.comments import CommentsTransform
6+
from postprocess.transforms.formatting import FormattingTransform
67

78

89
def get_transform_by_id(
@@ -16,5 +17,7 @@ def get_transform_by_id(
1617
return CommentsTransform(cache=cache, model=model)
1718
case "asserts":
1819
return AssertsTransform(cache=cache, model=model)
20+
case "formatting":
21+
return FormattingTransform(cache=cache, model=model)
1922
case _:
2023
raise ValueError(f"Unsupported transform: {id}")
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import logging
2+
import re
3+
from collections.abc import Callable
4+
from pathlib import Path
5+
from textwrap import dedent
6+
7+
from postprocess.cache import AbstractCache
8+
from postprocess.definitions import CDefinition, MergeRustError
9+
from postprocess.models import AbstractGenerativeModel, api_key_from_env
10+
from postprocess.transforms.base import (
11+
AbstractTransform,
12+
TransformCandidate,
13+
TransformError,
14+
)
15+
from postprocess.utils import get_highlighted_rust, remove_backticks
16+
17+
SYSTEM_INSTRUCTION = (
18+
"You are a helpful assistant that conservatively reformats c2rust-transpiled "
19+
"Rust functions for compactness while preserving idiomatic Rust formatting."
20+
)
21+
22+
23+
class FormattingTransformPrompt:
24+
c_function: str
25+
rust_function: str
26+
prompt_text: str
27+
identifier: str
28+
29+
__slots__ = ("c_function", "rust_function", "prompt_text", "identifier")
30+
31+
def __init__(
32+
self, c_function: str, rust_function: str, prompt_text: str, identifier: str
33+
):
34+
self.c_function = c_function
35+
self.rust_function = rust_function
36+
self.prompt_text = prompt_text
37+
self.identifier = identifier
38+
39+
def __str__(self) -> str:
40+
return (
41+
self.prompt_text
42+
+ "\n\n"
43+
+ "C function:\n```c\n"
44+
+ self.c_function
45+
+ "```\n\n"
46+
+ "Rust function:\n```rust\n"
47+
+ self.rust_function
48+
+ "```\n"
49+
)
50+
51+
52+
class FormattingTransform(AbstractTransform):
53+
def __init__(self, cache: AbstractCache, model: AbstractGenerativeModel):
54+
super().__init__(SYSTEM_INSTRUCTION)
55+
self.cache = cache
56+
self.model = model
57+
58+
@staticmethod
59+
def should_attempt_formatting(c_function: str, rust_function: str) -> bool:
60+
c_line_count = len([line for line in c_function.splitlines() if line.strip()])
61+
rust_line_count = len(
62+
[line for line in rust_function.splitlines() if line.strip()]
63+
)
64+
65+
if rust_line_count <= max(c_line_count * 2, c_line_count + 20):
66+
return False
67+
68+
compactable_item_lines = 0
69+
for line in rust_function.splitlines():
70+
stripped = line.strip()
71+
if len(stripped) <= 48 and re.fullmatch(r"[^,{};]+,", stripped):
72+
compactable_item_lines += 1
73+
74+
return " = [" in rust_function and compactable_item_lines >= 16
75+
76+
@staticmethod
77+
def get_validation_fn(identifier: str) -> Callable[[str], str]:
78+
def validate_response(rust_fn: str) -> str:
79+
rust_fn = remove_backticks(rust_fn).strip()
80+
81+
if not rust_fn:
82+
return (
83+
"FAILURE: Empty response. Reply with the full Rust function "
84+
"definition only; say nothing else."
85+
)
86+
87+
if "```" in rust_fn:
88+
return (
89+
"FAILURE: Response contains Markdown code fences. Reply with the "
90+
"full Rust function definition only; say nothing else."
91+
)
92+
93+
if f"fn {identifier}" not in rust_fn:
94+
return (
95+
f"FAILURE: Response does not contain function `{identifier}`. "
96+
"Reply with the full Rust function definition only; say nothing else." # noqa: E501
97+
)
98+
99+
return "SUCCESS: Function formatted correctly!"
100+
101+
return validate_response
102+
103+
def try_apply_ident(
104+
self,
105+
rust_source_file: Path,
106+
rust_definition: str,
107+
c_definition: CDefinition,
108+
identifier: str,
109+
attempt: int = 0,
110+
previous_error: MergeRustError | None = None,
111+
) -> TransformCandidate | None:
112+
_ = (rust_source_file, attempt)
113+
if not self.should_attempt_formatting(c_definition.effective, rust_definition):
114+
logging.info(
115+
f"{self.__class__.__name__}: "
116+
f"Skipping function without obvious compactness issue: {identifier}"
117+
)
118+
return
119+
120+
prompt_text = """
121+
Reformat the Rust function below only where the transpiled formatting is
122+
needlessly verbose compared with the corresponding C function.
123+
124+
Most Rust functions should stay exactly as rustfmt would format them. Make
125+
changes only for mechanically expanded tables, arrays, lookup data, or similar
126+
data-heavy structures where the Rust version is much longer than the C version
127+
because rustfmt placed one small element per line.
128+
129+
Requirements:
130+
- Preserve Rust syntax, behavior, attributes, signature, names, types,
131+
expressions, comments, and control flow.
132+
- Do not try to make ordinary Rust statements imitate C brace or indentation
133+
style. Keep ordinary code idiomatic for Rust.
134+
- For compacted data structures, take formatting clues from the C version:
135+
group comparable numbers of elements per line, keep related comments near the
136+
same data, and preserve useful visual structure.
137+
- Add #[rustfmt::skip] to the function if needed so rustfmt will not expand the
138+
compacted data structure again.
139+
- If there is no clear table/array/data-structure compactness problem, return
140+
the original Rust function unchanged.
141+
- Return the full Rust function definition only; say nothing else.
142+
"""
143+
prompt_text = dedent(prompt_text).strip()
144+
145+
prompt = FormattingTransformPrompt(
146+
c_function=c_definition.effective,
147+
rust_function=rust_definition,
148+
prompt_text=prompt_text,
149+
identifier=identifier,
150+
)
151+
152+
messages = [{"role": "user", "content": str(prompt)}]
153+
messages = self.with_merge_retry_message(messages, previous_error)
154+
155+
transform = self.__class__.__name__
156+
model = self.model.id
157+
158+
if response := self.cache.lookup(
159+
transform=transform,
160+
identifier=identifier,
161+
model=model,
162+
messages=messages,
163+
):
164+
rust_fn = remove_backticks(response)
165+
return TransformCandidate(
166+
identifier=identifier,
167+
messages=messages,
168+
response=response,
169+
definition=rust_fn,
170+
)
171+
172+
validate_response = self.get_validation_fn(identifier)
173+
174+
for _attempt in range(3):
175+
response = self.model.generate_with_tools(
176+
messages, tools=[validate_response]
177+
)
178+
if response is None:
179+
if api_key_from_env(model) is None:
180+
logging.warning(
181+
f"Cache miss for {identifier}; "
182+
"skipping since no API key was set..."
183+
)
184+
return
185+
logging.warning("Model returned no response")
186+
continue
187+
188+
validation_result = validate_response(response)
189+
if not validation_result.startswith("SUCCESS"):
190+
logging.warning(
191+
f"Model response for {identifier} failed validation: "
192+
+ validation_result
193+
+ "\nResponse was:\n"
194+
+ response
195+
)
196+
continue
197+
198+
break
199+
else:
200+
raise TransformError(
201+
f"Model failed to produce valid response after multiple "
202+
f"attempts for {identifier}"
203+
)
204+
205+
rust_fn = remove_backticks(response)
206+
if rust_fn == rust_definition:
207+
logging.warning(
208+
f"{self.__class__.__name__}: "
209+
f"No formatting changes for function: {identifier}"
210+
)
211+
else:
212+
logging.info(
213+
f"Formatted Rust fn {identifier}:\
214+
\n{get_highlighted_rust(rust_fn)}"
215+
)
216+
217+
return TransformCandidate(
218+
identifier=identifier,
219+
messages=messages,
220+
response=response,
221+
definition=rust_fn,
222+
)

0 commit comments

Comments
 (0)