jarvis-ai-assistant 0.5.1__py3-none-any.whl → 0.6.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.
Files changed (29) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/jarvis_agent/__init__.py +15 -4
  3. jarvis/jarvis_agent/agent_manager.py +3 -0
  4. jarvis/jarvis_agent/jarvis.py +44 -14
  5. jarvis/jarvis_agent/run_loop.py +6 -1
  6. jarvis/jarvis_agent/task_planner.py +1 -0
  7. jarvis/jarvis_c2rust/__init__.py +13 -0
  8. jarvis/jarvis_c2rust/cli.py +405 -0
  9. jarvis/jarvis_c2rust/collector.py +209 -0
  10. jarvis/jarvis_c2rust/library_replacer.py +933 -0
  11. jarvis/jarvis_c2rust/llm_module_agent.py +1265 -0
  12. jarvis/jarvis_c2rust/scanner.py +1671 -0
  13. jarvis/jarvis_c2rust/transpiler.py +1236 -0
  14. jarvis/jarvis_code_agent/code_agent.py +144 -18
  15. jarvis/jarvis_data/config_schema.json +8 -3
  16. jarvis/jarvis_tools/cli/main.py +1 -0
  17. jarvis/jarvis_tools/execute_script.py +1 -1
  18. jarvis/jarvis_tools/read_code.py +11 -1
  19. jarvis/jarvis_tools/read_symbols.py +129 -0
  20. jarvis/jarvis_tools/registry.py +9 -1
  21. jarvis/jarvis_utils/config.py +14 -4
  22. jarvis/jarvis_utils/git_utils.py +39 -0
  23. jarvis/jarvis_utils/utils.py +13 -5
  24. {jarvis_ai_assistant-0.5.1.dist-info → jarvis_ai_assistant-0.6.0.dist-info}/METADATA +13 -1
  25. {jarvis_ai_assistant-0.5.1.dist-info → jarvis_ai_assistant-0.6.0.dist-info}/RECORD +29 -21
  26. {jarvis_ai_assistant-0.5.1.dist-info → jarvis_ai_assistant-0.6.0.dist-info}/entry_points.txt +2 -0
  27. {jarvis_ai_assistant-0.5.1.dist-info → jarvis_ai_assistant-0.6.0.dist-info}/WHEEL +0 -0
  28. {jarvis_ai_assistant-0.5.1.dist-info → jarvis_ai_assistant-0.6.0.dist-info}/licenses/LICENSE +0 -0
  29. {jarvis_ai_assistant-0.5.1.dist-info → jarvis_ai_assistant-0.6.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1236 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ C2Rust 转译器模块
4
+
5
+ 目标:
6
+ - 基于 scanner 生成的 translation_order.jsonl 顺序,逐个函数进行转译
7
+ - 为每个函数:
8
+ 1) 准备上下文:C 源码片段+位置信息、被调用符号(若已转译则提供Rust模块与符号,否则提供原C位置信息)、crate目录结构
9
+ 2) 创建“模块选择与签名Agent”:让其选择合适的Rust模块路径,并在summary输出函数签名
10
+ 3) 记录当前进度到 progress.json
11
+ 4) 基于上述信息与落盘位置,创建 CodeAgent 生成转译后的Rust函数
12
+ 5) 尝试 cargo build,如失败则携带错误上下文创建 CodeAgent 修复,直到构建通过或达到上限
13
+ 6) 创建代码审查Agent;若 summary 指出问题,则 CodeAgent 优化,直到 summary 表示无问题
14
+ 7) 标记函数已转译,并记录 C 符号 -> Rust 符号/模块映射到 symbol_map.jsonl(JSONL,每行一条映射,支持重复与重载)
15
+
16
+ 说明:
17
+ - 本模块提供 run_transpile(...) 作为对外入口,后续在 cli.py 中挂载为子命令
18
+ - 尽量复用现有 Agent/CodeAgent 能力,保持最小侵入与稳定性
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import os
24
+ import re
25
+ import subprocess
26
+ import time
27
+ from dataclasses import dataclass
28
+ from pathlib import Path
29
+ from typing import Any, Dict, List, Optional, Tuple, Union, Set
30
+
31
+ import typer
32
+
33
+ from jarvis.jarvis_c2rust.scanner import compute_translation_order_jsonl
34
+ from jarvis.jarvis_agent import Agent
35
+ from jarvis.jarvis_code_agent.code_agent import CodeAgent
36
+
37
+
38
+ # 数据文件常量
39
+ C2RUST_DIRNAME = ".jarvis/c2rust"
40
+
41
+ SYMBOLS_JSONL = "symbols.jsonl"
42
+ ORDER_JSONL = "translation_order.jsonl"
43
+ PROGRESS_JSON = "progress.json"
44
+ SYMBOL_MAP_JSONL = "symbol_map.jsonl"
45
+ # 兼容旧版:若存在 symbol_map.json 也尝试加载(只读)
46
+ LEGACY_SYMBOL_MAP_JSON = "symbol_map.json"
47
+
48
+
49
+ @dataclass
50
+ class FnRecord:
51
+ id: int
52
+ name: str
53
+ qname: str
54
+ file: str
55
+ start_line: int
56
+ start_col: int
57
+ end_line: int
58
+ end_col: int
59
+ refs: List[str]
60
+ # 来自库替代阶段的上下文元数据(若存在)
61
+ lib_replacement: Optional[Dict[str, Any]] = None
62
+
63
+
64
+ class _DbLoader:
65
+ """读取 symbols.jsonl 并提供按 id/name 查询与源码片段读取"""
66
+
67
+ def __init__(self, project_root: Union[str, Path]) -> None:
68
+ self.project_root = Path(project_root).resolve()
69
+ self.data_dir = self.project_root / C2RUST_DIRNAME
70
+
71
+ self.symbols_path = self.data_dir / SYMBOLS_JSONL
72
+ # 统一流程:仅使用 symbols.jsonl,不再兼容 functions.jsonl
73
+ if not self.symbols_path.exists():
74
+ raise FileNotFoundError(
75
+ f"在目录下未找到 symbols.jsonl: {self.data_dir}"
76
+ )
77
+
78
+ self.fn_by_id: Dict[int, FnRecord] = {}
79
+ self.name_to_id: Dict[str, int] = {}
80
+ self._load()
81
+
82
+ def _load(self) -> None:
83
+ """
84
+ 读取统一的 symbols.jsonl。
85
+ 不区分函数与类型定义,均加载为通用记录(位置与引用信息)。
86
+ """
87
+ def _iter_records_from_file(path: Path):
88
+ try:
89
+ with path.open("r", encoding="utf-8") as f:
90
+ idx = 0
91
+ for line in f:
92
+ line = line.strip()
93
+ if not line:
94
+ continue
95
+ try:
96
+ obj = json.loads(line)
97
+ except Exception:
98
+ continue
99
+ idx += 1
100
+ yield idx, obj
101
+ except FileNotFoundError:
102
+ return
103
+
104
+ # 加载所有符号记录(函数、类型等)
105
+ for idx, obj in _iter_records_from_file(self.symbols_path):
106
+ fid = int(obj.get("id") or idx)
107
+ nm = obj.get("name") or ""
108
+ qn = obj.get("qualified_name") or ""
109
+ fp = obj.get("file") or ""
110
+ refs = obj.get("ref")
111
+ # 统一使用列表类型的引用字段
112
+ if not isinstance(refs, list):
113
+ refs = []
114
+ refs = [c for c in refs if isinstance(c, str) and c]
115
+ sr = int(obj.get("start_line") or 0)
116
+ sc = int(obj.get("start_col") or 0)
117
+ er = int(obj.get("end_line") or 0)
118
+ ec = int(obj.get("end_col") or 0)
119
+ lr = obj.get("lib_replacement") if isinstance(obj.get("lib_replacement"), dict) else None
120
+ rec = FnRecord(
121
+ id=fid,
122
+ name=nm,
123
+ qname=qn,
124
+ file=fp,
125
+ start_line=sr,
126
+ start_col=sc,
127
+ end_line=er,
128
+ end_col=ec,
129
+ refs=refs,
130
+ lib_replacement=lr,
131
+ )
132
+ self.fn_by_id[fid] = rec
133
+ if nm:
134
+ self.name_to_id.setdefault(nm, fid)
135
+ if qn:
136
+ self.name_to_id.setdefault(qn, fid)
137
+
138
+ def get(self, fid: int) -> Optional[FnRecord]:
139
+ return self.fn_by_id.get(fid)
140
+
141
+ def get_id_by_name(self, name_or_qname: str) -> Optional[int]:
142
+ return self.name_to_id.get(name_or_qname)
143
+
144
+ def read_source_span(self, rec: FnRecord) -> str:
145
+ """按起止行读取源码片段(忽略列边界,尽量完整)"""
146
+ try:
147
+ p = Path(rec.file)
148
+ # 若记录为相对路径,基于 project_root 解析
149
+ if not p.is_absolute():
150
+ p = (self.project_root / p).resolve()
151
+ if not p.exists():
152
+ return ""
153
+ lines = p.read_text(encoding="utf-8", errors="replace").splitlines()
154
+ s = max(1, rec.start_line)
155
+ e = min(len(lines), max(rec.end_line, s))
156
+ # Python 索引从0开始,包含终止行
157
+ chunk = "\n".join(lines[s - 1 : e])
158
+ return chunk
159
+ except Exception:
160
+ return ""
161
+
162
+
163
+ class _SymbolMapJsonl:
164
+ """
165
+ JSONL 形式的符号映射管理:
166
+ - 每行一条记录,支持同名函数的多条映射(用于处理重载/同名符号)
167
+ - 记录字段:
168
+ {
169
+ "c_name": "<简单名>",
170
+ "c_qname": "<限定名,可为空字符串>",
171
+ "c_file": "<源文件路径>",
172
+ "start_line": <int>,
173
+ "end_line": <int>,
174
+ "module": "src/xxx.rs 或 src/xxx/mod.rs",
175
+ "rust_symbol": "<Rust函数名>",
176
+ "updated_at": "YYYY-MM-DDTHH:MM:SS"
177
+ }
178
+ - 提供按名称(c_name/c_qname)查询、按源位置判断是否已记录等能力
179
+ """
180
+
181
+ def __init__(self, jsonl_path: Path, legacy_json_path: Optional[Path] = None) -> None:
182
+ self.jsonl_path = jsonl_path
183
+ self.legacy_json_path = legacy_json_path
184
+ self.records: List[Dict[str, Any]] = []
185
+ # 索引:名称 -> 记录列表索引
186
+ self.by_key: Dict[str, List[int]] = {}
187
+ # 唯一定位(避免同名冲突):(c_file, start_line, end_line, c_qname or c_name) -> 记录索引列表
188
+ self.by_pos: Dict[Tuple[str, int, int, str], List[int]] = {}
189
+ self._load()
190
+
191
+ def _load(self) -> None:
192
+ self.records = []
193
+ self.by_key = {}
194
+ self.by_pos = {}
195
+ # 读取 JSONL
196
+ if self.jsonl_path.exists():
197
+ try:
198
+ with self.jsonl_path.open("r", encoding="utf-8") as f:
199
+ for line in f:
200
+ line = line.strip()
201
+ if not line:
202
+ continue
203
+ try:
204
+ obj = json.loads(line)
205
+ except Exception:
206
+ continue
207
+ self._add_record_in_memory(obj)
208
+ except Exception:
209
+ pass
210
+ # 兼容旧版 symbol_map.json(若存在则读入为“最后一条”)
211
+ elif self.legacy_json_path and self.legacy_json_path.exists():
212
+ try:
213
+ legacy = json.loads(self.legacy_json_path.read_text(encoding="utf-8"))
214
+ if isinstance(legacy, dict):
215
+ for k, v in legacy.items():
216
+ rec = {
217
+ "c_name": k,
218
+ "c_qname": k,
219
+ "c_file": "",
220
+ "start_line": 0,
221
+ "end_line": 0,
222
+ "module": v.get("module"),
223
+ "rust_symbol": v.get("rust_symbol"),
224
+ "updated_at": v.get("updated_at"),
225
+ }
226
+ self._add_record_in_memory(rec)
227
+ except Exception:
228
+ pass
229
+
230
+ def _add_record_in_memory(self, rec: Dict[str, Any]) -> None:
231
+ idx = len(self.records)
232
+ self.records.append(rec)
233
+ for key in [rec.get("c_name") or "", rec.get("c_qname") or ""]:
234
+ k = str(key or "").strip()
235
+ if not k:
236
+ continue
237
+ self.by_key.setdefault(k, []).append(idx)
238
+ pos_key = (str(rec.get("c_file") or ""), int(rec.get("start_line") or 0), int(rec.get("end_line") or 0), str(rec.get("c_qname") or rec.get("c_name") or ""))
239
+ self.by_pos.setdefault(pos_key, []).append(idx)
240
+
241
+ def has_symbol(self, sym: str) -> bool:
242
+ return bool(self.by_key.get(sym))
243
+
244
+ def get(self, sym: str) -> List[Dict[str, Any]]:
245
+ idxs = self.by_key.get(sym) or []
246
+ return [self.records[i] for i in idxs]
247
+
248
+ def get_any(self, sym: str) -> Optional[Dict[str, Any]]:
249
+ recs = self.get(sym)
250
+ return recs[-1] if recs else None
251
+
252
+ def has_rec(self, rec: FnRecord) -> bool:
253
+ key = (str(rec.file or ""), int(rec.start_line or 0), int(rec.end_line or 0), str(rec.qname or rec.name or ""))
254
+ return bool(self.by_pos.get(key))
255
+
256
+ def add(self, rec: FnRecord, module: str, rust_symbol: str) -> None:
257
+ obj = {
258
+ "c_name": rec.name or "",
259
+ "c_qname": rec.qname or "",
260
+ "c_file": rec.file or "",
261
+ "start_line": int(rec.start_line or 0),
262
+ "end_line": int(rec.end_line or 0),
263
+ "module": str(module or ""),
264
+ "rust_symbol": str(rust_symbol or (rec.name or f"fn_{rec.id}")),
265
+ "updated_at": time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime()),
266
+ }
267
+ # 先写盘,再更新内存索引
268
+ try:
269
+ self.jsonl_path.parent.mkdir(parents=True, exist_ok=True)
270
+ with self.jsonl_path.open("a", encoding="utf-8") as f:
271
+ f.write(json.dumps(obj, ensure_ascii=False) + "\n")
272
+ except Exception:
273
+ pass
274
+ self._add_record_in_memory(obj)
275
+
276
+
277
+ def _ensure_order_file(project_root: Path) -> Path:
278
+ """确保 translation_order.jsonl 存在且包含有效步骤;仅基于 symbols.jsonl 生成,不使用任何回退。"""
279
+ data_dir = project_root / C2RUST_DIRNAME
280
+ order_path = data_dir / ORDER_JSONL
281
+
282
+ def _has_steps(p: Path) -> bool:
283
+ try:
284
+ steps = _iter_order_steps(p)
285
+ return bool(steps)
286
+ except Exception:
287
+ return False
288
+
289
+ # 已存在则校验是否有步骤
290
+ if order_path.exists():
291
+ if _has_steps(order_path):
292
+ return order_path
293
+ # 为空或不可读:基于标准路径重新计算(仅 symbols.jsonl)
294
+ try:
295
+ compute_translation_order_jsonl(data_dir, out_path=order_path)
296
+ except Exception as e:
297
+ raise RuntimeError(f"重新计算翻译顺序失败: {e}")
298
+ return order_path
299
+
300
+ # 不存在:按标准路径生成到固定文件名(仅 symbols.jsonl)
301
+ try:
302
+ compute_translation_order_jsonl(data_dir, out_path=order_path)
303
+ except Exception as e:
304
+ raise RuntimeError(f"计算翻译顺序失败: {e}")
305
+
306
+ if not order_path.exists():
307
+ raise FileNotFoundError(f"计算后未找到 translation_order.jsonl: {order_path}")
308
+
309
+ # 最终校验:若仍无有效步骤,直接报错并提示先执行 scan 或检查 symbols.jsonl
310
+ if not _has_steps(order_path):
311
+ raise RuntimeError("translation_order.jsonl 无有效步骤。请先执行 'jarvis-c2rust scan' 生成 symbols.jsonl 并重试。")
312
+
313
+ return order_path
314
+
315
+ def _iter_order_steps(order_jsonl: Path) -> List[List[int]]:
316
+ """
317
+ 读取翻译顺序(兼容新旧格式),返回按步骤的函数id序列列表。
318
+ 新格式:每行包含 "ids": [int, ...] 以及 "items": [完整符号对象,...]。
319
+ 不再兼容旧格式(不支持 "records"/"symbols" 键)。
320
+ """
321
+ # 旧格式已移除:不再需要基于 symbols.jsonl 的 name->id 映射
322
+
323
+ steps: List[List[int]] = []
324
+ with order_jsonl.open("r", encoding="utf-8") as f:
325
+ for ln in f:
326
+ ln = ln.strip()
327
+ if not ln:
328
+ continue
329
+ try:
330
+ obj = json.loads(ln)
331
+ except Exception:
332
+ continue
333
+
334
+ ids = obj.get("ids")
335
+ if isinstance(ids, list) and ids:
336
+ # 新格式:仅支持 ids
337
+ try:
338
+ ids_int = [int(x) for x in ids if isinstance(x, (int, str)) and str(x).strip()]
339
+ except Exception:
340
+ ids_int = []
341
+ if ids_int:
342
+ steps.append(ids_int)
343
+ continue
344
+ # 不支持旧格式(无 ids 则跳过该行)
345
+ return steps
346
+
347
+
348
+ def _dir_tree(root: Path) -> str:
349
+ """格式化 crate 目录结构(过滤部分常见目录)"""
350
+ lines: List[str] = []
351
+ exclude = {".git", "target", ".jarvis"}
352
+ if not root.exists():
353
+ return ""
354
+ for p in sorted(root.rglob("*")):
355
+ if any(part in exclude for part in p.parts):
356
+ continue
357
+ rel = p.relative_to(root)
358
+ depth = len(rel.parts) - 1
359
+ indent = " " * depth
360
+ name = rel.name + ("/" if p.is_dir() else "")
361
+ lines.append(f"{indent}- {name}")
362
+ return "\n".join(lines)
363
+
364
+
365
+ def _default_crate_dir(project_root: Path) -> Path:
366
+ """遵循 llm_module_agent 的默认crate目录选择:<parent>/<cwd.name>_rs(与当前目录同级)当传入为 '.' 时"""
367
+ try:
368
+ cwd = Path(".").resolve()
369
+ if project_root.resolve() == cwd:
370
+ return cwd.parent / f"{cwd.name}_rs"
371
+ else:
372
+ return project_root
373
+ except Exception:
374
+ return project_root
375
+
376
+
377
+ def _read_json(path: Path, default: Any) -> Any:
378
+ try:
379
+ if path.exists():
380
+ return json.loads(path.read_text(encoding="utf-8"))
381
+ except Exception:
382
+ pass
383
+ return default
384
+
385
+
386
+ def _write_json(path: Path, obj: Any) -> None:
387
+ try:
388
+ path.parent.mkdir(parents=True, exist_ok=True)
389
+ path.write_text(json.dumps(obj, ensure_ascii=False, indent=2), encoding="utf-8")
390
+ except Exception:
391
+ pass
392
+
393
+
394
+ def _extract_json_from_summary(text: str) -> Dict[str, Any]:
395
+ """
396
+ 从 Agent summary 中提取结构化数据(仅支持 YAML):
397
+ - 仅在 <SUMMARY>...</SUMMARY> 块内查找;
398
+ - 只接受 <yaml>...</yaml> 标签包裹的 YAML 对象;
399
+ - 若未找到或解析失败,返回 {}。
400
+ """
401
+ if not isinstance(text, str) or not text.strip():
402
+ return {}
403
+
404
+ # 提取 <SUMMARY> 块
405
+ m = re.search(r"<SUMMARY>([\s\S]*?)</SUMMARY>", text, flags=re.IGNORECASE)
406
+ block = (m.group(1) if m else text).strip()
407
+
408
+ # 仅解析 <yaml>...</yaml>
409
+ mm = re.search(r"<yaml>([\s\S]*?)</yaml>", block, flags=re.IGNORECASE)
410
+ raw_yaml = mm.group(1).strip() if mm else None
411
+ if not raw_yaml:
412
+ return {}
413
+
414
+ try:
415
+ import yaml # type: ignore
416
+ obj = yaml.safe_load(raw_yaml)
417
+ return obj if isinstance(obj, dict) else {}
418
+ except Exception:
419
+ return {}
420
+
421
+
422
+ class Transpiler:
423
+ def __init__(
424
+ self,
425
+ project_root: Union[str, Path] = ".",
426
+ crate_dir: Optional[Union[str, Path]] = None,
427
+ llm_group: Optional[str] = None,
428
+ max_retries: int = 0,
429
+ resume: bool = True,
430
+ only: Optional[List[str]] = None, # 仅转译指定函数名(简单名或限定名)
431
+ ) -> None:
432
+ self.project_root = Path(project_root).resolve()
433
+ self.data_dir = self.project_root / C2RUST_DIRNAME
434
+ self.progress_path = self.data_dir / PROGRESS_JSON
435
+ # JSONL 路径
436
+ self.symbol_map_path = self.data_dir / SYMBOL_MAP_JSONL
437
+ # 兼容旧版 JSON 字典格式
438
+ self.legacy_symbol_map_path = self.data_dir / LEGACY_SYMBOL_MAP_JSON
439
+ self.llm_group = llm_group
440
+ self.max_retries = max_retries
441
+ self.resume = resume
442
+ self.only = set(only or [])
443
+
444
+ self.crate_dir = Path(crate_dir) if crate_dir else _default_crate_dir(self.project_root)
445
+ # 使用自包含的 order.jsonl 记录构建索引,避免依赖 symbols.jsonl
446
+ self.fn_index_by_id: Dict[int, FnRecord] = {}
447
+ self.fn_name_to_id: Dict[str, int] = {}
448
+
449
+ self.progress: Dict[str, Any] = _read_json(self.progress_path, {"current": None, "converted": []})
450
+ # 使用 JSONL 存储的符号映射
451
+ self.symbol_map = _SymbolMapJsonl(self.symbol_map_path, legacy_json_path=self.legacy_symbol_map_path)
452
+
453
+
454
+ def _save_progress(self) -> None:
455
+ _write_json(self.progress_path, self.progress)
456
+
457
+ # JSONL 模式下不再整体写回 symbol_map;此方法保留占位(兼容旧调用),无操作
458
+ def _save_symbol_map(self) -> None:
459
+ return
460
+
461
+ def _read_source_span(self, rec: FnRecord) -> str:
462
+ """按起止行读取源码片段(忽略列边界,尽量完整)"""
463
+ try:
464
+ p = Path(rec.file)
465
+ if not p.is_absolute():
466
+ p = (self.project_root / p).resolve()
467
+ if not p.exists():
468
+ return ""
469
+ lines = p.read_text(encoding="utf-8", errors="replace").splitlines()
470
+ s = max(1, int(rec.start_line or 1))
471
+ e = min(len(lines), max(int(rec.end_line or s), s))
472
+ chunk = "\n".join(lines[s - 1 : e])
473
+ return chunk
474
+ except Exception:
475
+ return ""
476
+
477
+ def _load_order_index(self, order_jsonl: Path) -> None:
478
+ """
479
+ 从自包含的 order.jsonl 中加载所有 records,建立:
480
+ - fn_index_by_id: id -> FnRecord
481
+ - fn_name_to_id: name/qname -> id
482
+ 若同一 id 多次出现,首次记录为准。
483
+ """
484
+ self.fn_index_by_id.clear()
485
+ self.fn_name_to_id.clear()
486
+ try:
487
+ with order_jsonl.open("r", encoding="utf-8") as f:
488
+ for ln in f:
489
+ ln = ln.strip()
490
+ if not ln:
491
+ continue
492
+ try:
493
+ obj = json.loads(ln)
494
+ except Exception:
495
+ continue
496
+ # 仅支持新格式:items
497
+ recs = obj.get("items")
498
+ if not isinstance(recs, list):
499
+ continue
500
+ for r in recs:
501
+ if not isinstance(r, dict):
502
+ continue
503
+ # 构建 FnRecord
504
+ try:
505
+ fid = int(r.get("id"))
506
+ except Exception:
507
+ continue
508
+ if fid in self.fn_index_by_id:
509
+ # 已收录
510
+ continue
511
+ nm = r.get("name") or ""
512
+ qn = r.get("qualified_name") or ""
513
+ fp = r.get("file") or ""
514
+ refs = r.get("ref")
515
+ if not isinstance(refs, list):
516
+ refs = []
517
+ refs = [c for c in refs if isinstance(c, str) and c]
518
+ sr = int(r.get("start_line") or 0)
519
+ sc = int(r.get("start_col") or 0)
520
+ er = int(r.get("end_line") or 0)
521
+ ec = int(r.get("end_col") or 0)
522
+ lr = r.get("lib_replacement") if isinstance(r.get("lib_replacement"), dict) else None
523
+ rec = FnRecord(
524
+ id=fid,
525
+ name=nm,
526
+ qname=qn,
527
+ file=fp,
528
+ start_line=sr,
529
+ start_col=sc,
530
+ end_line=er,
531
+ end_col=ec,
532
+ refs=refs,
533
+ lib_replacement=lr,
534
+ )
535
+ self.fn_index_by_id[fid] = rec
536
+ if nm:
537
+ self.fn_name_to_id.setdefault(nm, fid)
538
+ if qn:
539
+ self.fn_name_to_id.setdefault(qn, fid)
540
+ except Exception:
541
+ # 若索引构建失败,保持为空,后续流程将跳过
542
+ pass
543
+
544
+ def _should_skip(self, rec: FnRecord) -> bool:
545
+ # 如果 only 列表非空,则仅处理匹配者
546
+ if self.only:
547
+ if rec.name in self.only or rec.qname in self.only:
548
+ pass
549
+ else:
550
+ return True
551
+ # 已转译的跳过(按源位置与名称唯一性判断,避免同名不同位置的误判)
552
+ if self.symbol_map.has_rec(rec):
553
+ return True
554
+ return False
555
+
556
+ def _collect_callees_context(self, rec: FnRecord) -> List[Dict[str, Any]]:
557
+ """
558
+ 生成被引用符号上下文列表(不区分函数与类型):
559
+ - 若已转译:提供 {name, qname, translated: true, rust_module, rust_symbol, ambiguous?}
560
+ - 若未转译但存在扫描记录:提供 {name, qname, translated: false, file, start_line, end_line}
561
+ - 若仅名称:提供 {name, qname, translated: false}
562
+ 注:若存在同名映射多条记录(重载/同名符号),此处标记 ambiguous=true,并选择最近一条作为提示。
563
+ """
564
+ ctx: List[Dict[str, Any]] = []
565
+ for callee in rec.refs or []:
566
+ entry: Dict[str, Any] = {"name": callee, "qname": callee}
567
+ # 已转译映射
568
+ if self.symbol_map.has_symbol(callee):
569
+ recs = self.symbol_map.get(callee)
570
+ m = recs[-1] if recs else None
571
+ entry.update({
572
+ "translated": True,
573
+ "rust_module": (m or {}).get("module"),
574
+ "rust_symbol": (m or {}).get("rust_symbol"),
575
+ })
576
+ if len(recs) > 1:
577
+ entry["ambiguous"] = True
578
+ ctx.append(entry)
579
+ continue
580
+ # 使用 order 索引按名称解析ID(函数或类型)
581
+ cid = self.fn_name_to_id.get(callee)
582
+ if cid:
583
+ crec = self.fn_index_by_id.get(cid)
584
+ if crec:
585
+ entry.update({
586
+ "translated": False,
587
+ "file": crec.file,
588
+ "start_line": crec.start_line,
589
+ "end_line": crec.end_line,
590
+ })
591
+ else:
592
+ entry.update({"translated": False})
593
+ ctx.append(entry)
594
+ return ctx
595
+
596
+ def _untranslated_callee_symbols(self, rec: FnRecord) -> List[str]:
597
+ """
598
+ 返回尚未转换的被调函数符号(使用扫描记录中的名称/限定名作为键)
599
+ """
600
+ syms: List[str] = []
601
+ for callee in rec.refs or []:
602
+ if not self.symbol_map.has_symbol(callee):
603
+ syms.append(callee)
604
+ # 去重
605
+ try:
606
+ syms = list(dict.fromkeys(syms))
607
+ except Exception:
608
+ syms = sorted(list(set(syms)))
609
+ return syms
610
+
611
+ def _build_module_selection_prompts(
612
+ self,
613
+ rec: FnRecord,
614
+ c_code: str,
615
+ callees_ctx: List[Dict[str, Any]],
616
+ crate_tree: str,
617
+ ) -> Tuple[str, str, str]:
618
+ """
619
+ 返回 (system_prompt, user_prompt, summary_prompt)
620
+ 要求 summary 输出 YAML:
621
+ {
622
+ "module": "src/<path>.rs or module path (e.g., src/foo/mod.rs or src/foo/bar.rs)",
623
+ "rust_signature": "pub fn ...",
624
+ "notes": "optional"
625
+ }
626
+ """
627
+ system_prompt = (
628
+ "你是资深Rust工程师,擅长为C/C++函数选择合适的Rust模块位置并产出对应的Rust函数签名。\n"
629
+ "目标:根据提供的C源码、调用者上下文与crate目录结构,为该函数选择合适的Rust模块文件并给出Rust函数签名(不实现)。\n"
630
+ "原则:\n"
631
+ "- 按功能内聚与依赖方向选择模块,避免循环依赖;\n"
632
+ "- 模块路径必须落在 crate 的 src/ 下,优先放置到已存在的模块中;必要时可建议创建新的子模块文件;\n"
633
+ "- 函数签名请尽量在Rust中表达指针/数组/结构体语义(可先用简单类型占位,后续由实现阶段细化);\n"
634
+ "- 仅输出必要信息,避免冗余解释。"
635
+ )
636
+ user_prompt = "\n".join([
637
+ "请阅读以下上下文并准备总结:",
638
+ f"- 函数标识: id={rec.id}, name={rec.name}, qualified={rec.qname}",
639
+ f"- 源文件位置: {rec.file}:{rec.start_line}-{rec.end_line}",
640
+ f"- crate 根目录路径: {self.crate_dir.resolve()}",
641
+ "",
642
+ "C函数源码片段:",
643
+ "<C_SOURCE>",
644
+ c_code,
645
+ "</C_SOURCE>",
646
+ "",
647
+ "被引用符号上下文(如已转译则包含Rust模块信息):",
648
+ json.dumps(callees_ctx, ensure_ascii=False, indent=2),
649
+ "",
650
+ "库替代上下文(若存在):",
651
+ json.dumps(getattr(rec, "lib_replacement", None), ensure_ascii=False, indent=2),
652
+ "",
653
+ "当前crate目录结构(部分):",
654
+ "<CRATE_TREE>",
655
+ crate_tree,
656
+ "</CRATE_TREE>",
657
+ "",
658
+ "为避免完整读取体积较大的符号表,你也可以使用工具 read_symbols 按需获取指定符号记录:",
659
+ f"- 工具: read_symbols",
660
+ "- 参数示例(YAML):",
661
+ f" symbols_file: \"{(self.data_dir / 'symbols.jsonl').resolve()}\"",
662
+ " symbols:",
663
+ " - 要读取的符号列表",
664
+ "",
665
+ "如果理解完毕,请进入总结阶段。",
666
+ ])
667
+ summary_prompt = (
668
+ "请仅输出一个 <SUMMARY> 块,块内必须且只包含一个 <yaml>...</yaml>,不得包含其它内容。\n"
669
+ "允许字段(YAML 对象):\n"
670
+ '- module: "<绝对路径>/src/xxx.rs 或 <绝对路径>/src/xxx/mod.rs"\n'
671
+ '- rust_signature: "pub fn xxx(...)->..."\n'
672
+ '- notes: "可选说明(若有上下文缺失或风险点,请在此列出)"\n'
673
+ "注意:\n"
674
+ "- module 必须位于 crate 的 src/ 目录下且使用绝对路径;尽量选择已有文件;如需新建文件,给出合理路径;\n"
675
+ "- rust_signature 请包含可见性修饰与函数名(可先用占位类型)。\n"
676
+ "请严格按以下格式输出:\n"
677
+ "<SUMMARY><yaml>\nmodule: \"...\"\nrust_signature: \"...\"\nnotes: \"...\"\n</yaml></SUMMARY>"
678
+ )
679
+ return system_prompt, user_prompt, summary_prompt
680
+
681
+ def _plan_module_and_signature(self, rec: FnRecord, c_code: str) -> Tuple[str, str]:
682
+ """调用 Agent 选择模块与签名,返回 (module_path, rust_signature),若格式不满足将自动重试直到满足"""
683
+ crate_tree = _dir_tree(self.crate_dir)
684
+ callees_ctx = self._collect_callees_context(rec)
685
+ sys_p, usr_p, base_sum_p = self._build_module_selection_prompts(rec, c_code, callees_ctx, crate_tree)
686
+
687
+ def _validate(meta: Any) -> Tuple[bool, str]:
688
+ if not isinstance(meta, dict) or not meta:
689
+ return False, "未解析到有效的 <SUMMARY><yaml> 对象"
690
+ module = meta.get("module")
691
+ rust_sig = meta.get("rust_signature")
692
+ if not isinstance(module, str) or not module.strip():
693
+ return False, "缺少必填字段 module"
694
+ if not isinstance(rust_sig, str) or not rust_sig.strip():
695
+ return False, "缺少必填字段 rust_signature"
696
+ try:
697
+ mp = Path(str(module).strip()).resolve()
698
+ if not mp.is_absolute():
699
+ return False, "module 必须为绝对路径"
700
+ crate_root = self.crate_dir.resolve()
701
+ # 必须位于 crate/src 下
702
+ rel = mp.relative_to(crate_root)
703
+ parts = rel.parts
704
+ if not parts or parts[0] != "src":
705
+ return False, "module 必须位于 crate 的 src/ 目录下(绝对路径)"
706
+ except Exception:
707
+ return False, "module 路径不可解析或不在 crate/src 下"
708
+ if not re.search(r"\bfn\s+[A-Za-z_][A-Za-z0-9_]*\s*\(", rust_sig):
709
+ return False, "rust_signature 无效:未检测到 Rust 函数签名(fn 名称)"
710
+ return True, ""
711
+
712
+ def _retry_sum_prompt(reason: str) -> str:
713
+ return (
714
+ base_sum_p
715
+ + "\n\n[格式校验失败,必须重试]\n"
716
+ + f"- 失败原因:{reason}\n"
717
+ + "- 仅输出一个 <SUMMARY> 块;块内仅包含单个 <yaml> 对象;\n"
718
+ + '- YAML 对象必须包含字段:module(位于 src/ 下)、rust_signature(形如 "pub fn name(...)")。\n'
719
+ )
720
+
721
+ attempt = 0
722
+ last_reason = "未知错误"
723
+ while True:
724
+ attempt += 1
725
+ sum_p = base_sum_p if attempt == 1 else _retry_sum_prompt(last_reason)
726
+
727
+ agent = Agent(
728
+ system_prompt=sys_p,
729
+ name="C2Rust-Function-Planner",
730
+ model_group=self.llm_group,
731
+ summary_prompt=sum_p,
732
+ need_summary=True,
733
+ auto_complete=True,
734
+ use_tools=["execute_script", "read_code", "retrieve_memory", "save_memory", "read_symbols"],
735
+ plan=False,
736
+ non_interactive=True,
737
+ use_methodology=False,
738
+ use_analysis=False,
739
+ disable_file_edit=True,
740
+ )
741
+ prev_cwd = os.getcwd()
742
+ try:
743
+ os.chdir(str(self.crate_dir))
744
+ summary = agent.run(usr_p)
745
+ finally:
746
+ os.chdir(prev_cwd)
747
+ meta = _extract_json_from_summary(str(summary or ""))
748
+ ok, reason = _validate(meta)
749
+ if ok:
750
+ module = str(meta.get("module") or "").strip()
751
+ rust_sig = str(meta.get("rust_signature") or "").strip()
752
+ return module, rust_sig
753
+ else:
754
+ last_reason = reason
755
+
756
+ def _update_progress_current(self, rec: FnRecord, module: str, rust_sig: str) -> None:
757
+ self.progress["current"] = {
758
+ "time": time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime()),
759
+ "id": rec.id,
760
+ "name": rec.name,
761
+ "qualified_name": rec.qname,
762
+ "file": rec.file,
763
+ "start_line": rec.start_line,
764
+ "end_line": rec.end_line,
765
+ "module": module,
766
+ "rust_signature": rust_sig,
767
+ }
768
+ self._save_progress()
769
+
770
+ def _codeagent_generate_impl(self, rec: FnRecord, c_code: str, module: str, rust_sig: str, unresolved: List[str]) -> None:
771
+ """
772
+ 使用 CodeAgent 生成/更新目标模块中的函数实现。
773
+ 约束:最小变更,生成可编译的占位实现,尽可能保留后续细化空间。
774
+ """
775
+ symbols_path = str((self.data_dir / "symbols.jsonl").resolve())
776
+ requirement_lines = [
777
+ f"目标:在 crate 目录 {self.crate_dir.resolve()} 的 {module} 中,为 C 函数 {rec.qname or rec.name} 生成对应的 Rust 实现。",
778
+ "要求:",
779
+ f"- 函数签名(建议):{rust_sig}",
780
+ "- 若 module 文件不存在则新建;为所在模块添加必要的 mod 声明(若需要);",
781
+ "- 若已有函数占位/实现,尽量最小修改,不要破坏现有代码;",
782
+ "- 你可以参考原 C 函数的关联实现(如同文件/同模块的相关辅助函数、内联实现、宏与注释等),在保持语义一致的前提下以符合 Rust 风格的方式实现;避免机械复制粘贴;",
783
+ "- 禁止在函数实现中使用 todo!/unimplemented! 作为占位;仅当调用的函数尚未实现时,才在调用位置使用 todo!(\"符号名\") 占位;",
784
+ "- 为保证 cargo build 通过,如需返回占位值,请使用合理默认值或 Result/Option 等,而非 panic!/todo!/unimplemented!;",
785
+ "- 不要删除或移动其他无关文件。",
786
+ "",
787
+ "编码原则与规范:",
788
+ "- 保持最小变更,避免无关重构与格式化;禁止批量重排/重命名/移动文件;",
789
+ "- 命名遵循Rust惯例(函数/模块蛇形命名),公共API使用pub;",
790
+ "- 优先使用安全Rust;如需unsafe,将范围最小化并添加注释说明原因与SAFETY保证;",
791
+ "- 错误处理:可暂用 Result<_, anyhow::Error> 或 Option 作占位,避免 panic!/unwrap();",
792
+ "- 实现中禁止使用 todo!/unimplemented! 占位;仅允许为尚未实现的被调符号在调用位置使用 todo!(\"符号名\");",
793
+ "- 如需占位返回,使用合理默认值或 Result/Option 等而非 panic!/todo!/unimplemented!;",
794
+ "- 依赖未实现符号时,务必使用 todo!(\"符号名\") 明确标记,便于后续自动替换;",
795
+ "- 文档:为新增函数添加简要文档注释,注明来源C函数与意图;",
796
+ "- 风格:遵循 rustfmt 默认风格,避免引入与本次改动无关的大范围格式变化;",
797
+ "- 输出限制:仅以补丁形式修改目标文件,不要输出解释或多余文本。",
798
+ "",
799
+ "C 源码片段(供参考,不要原样粘贴):",
800
+ "<C_SOURCE>",
801
+ c_code,
802
+ "</C_SOURCE>",
803
+ "",
804
+ "注意:所有修改均以补丁方式进行。",
805
+ "",
806
+ "如对实现细节不确定:可以使用工具 read_symbols 按需获取指定符号记录:",
807
+ f"- 工具: read_symbols",
808
+ "- 参数示例(YAML):",
809
+ f" symbols_file: \"{symbols_path}\"",
810
+ " symbols:",
811
+ " - 要读取的符号列表",
812
+ "",
813
+ "尚未转换的被调符号如下(请在调用位置使用 todo!(\"<符号>\") 作为占位,以便后续自动消除):",
814
+ *[f"- {s}" for s in (unresolved or [])],
815
+ ]
816
+ # 若存在库替代上下文,则附加到实现提示中,便于生成器参考(多库组合、参考API、备注等)
817
+ librep_ctx = None
818
+ try:
819
+ librep_ctx = getattr(rec, "lib_replacement", None)
820
+ except Exception:
821
+ librep_ctx = None
822
+ if isinstance(librep_ctx, dict) and librep_ctx:
823
+ requirement_lines.extend([
824
+ "",
825
+ "库替代上下文(若存在):",
826
+ json.dumps(librep_ctx, ensure_ascii=False, indent=2),
827
+ "",
828
+ ])
829
+ prompt = "\n".join(requirement_lines)
830
+ # 切换到 crate 目录运行 CodeAgent,运行完毕后恢复
831
+ prev_cwd = os.getcwd()
832
+ try:
833
+ os.chdir(str(self.crate_dir))
834
+ agent = CodeAgent(need_summary=False, non_interactive=True, plan=False, model_group=self.llm_group)
835
+ agent.run(prompt, prefix="[c2rust-transpiler][gen]", suffix="")
836
+ finally:
837
+ os.chdir(prev_cwd)
838
+
839
+ def _extract_rust_fn_name_from_sig(self, rust_sig: str) -> str:
840
+ """
841
+ 从 rust 签名中提取函数名,例如: 'pub fn foo(a: i32) -> i32 { ... }' -> 'foo'
842
+ """
843
+ m = re.search(r"\bfn\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(", rust_sig or "")
844
+ return m.group(1) if m else ""
845
+
846
+ def _module_file_to_crate_path(self, module: str) -> str:
847
+ """
848
+ 将模块文件路径转换为 crate 路径前缀:
849
+ - src/lib.rs -> crate
850
+ - src/foo/mod.rs -> crate::foo
851
+ - src/foo/bar.rs -> crate::foo::bar
852
+ 其它(非 src/ 前缀)统一返回 'crate'
853
+ """
854
+ mod = str(module).strip()
855
+ if not mod.startswith("src/"):
856
+ return "crate"
857
+ p = mod[len("src/"):]
858
+ if p.endswith("mod.rs"):
859
+ p = p[: -len("mod.rs")]
860
+ elif p.endswith(".rs"):
861
+ p = p[: -len(".rs")]
862
+ p = p.strip("/")
863
+ return "crate" if not p else "crate::" + p.replace("/", "::")
864
+
865
+ def _resolve_pending_todos_for_symbol(self, symbol: str, callee_module: str, callee_rust_fn: str, callee_rust_sig: str) -> None:
866
+ """
867
+ 当某个 C 符号对应的函数已转换为 Rust 后:
868
+ - 扫描整个 crate(优先 src/ 目录)中所有 .rs 文件,查找 todo!("符号名") 占位
869
+ - 对每个命中的文件,创建 CodeAgent 将占位替换为对已转换函数的真实调用(可使用 crate::... 完全限定路径或 use 引入)
870
+ - 最小化修改,避免无关重构
871
+
872
+ 说明:不再使用 todos.json,本方法直接搜索源码中的 todo!("xxxx")。
873
+ """
874
+ if not symbol:
875
+ return
876
+
877
+ # 计算被调函数的crate路径前缀,便于在提示中提供调用路径建议
878
+ callee_path = self._module_file_to_crate_path(callee_module)
879
+
880
+ # 扫描 src 下的 .rs 文件,查找 todo!("symbol") 占位
881
+ matches: List[str] = []
882
+ src_root = (self.crate_dir / "src").resolve()
883
+ if src_root.exists():
884
+ for p in sorted(src_root.rglob("*.rs")):
885
+ try:
886
+ text = p.read_text(encoding="utf-8", errors="replace")
887
+ except Exception:
888
+ continue
889
+ needle = f'todo!("{symbol}")'
890
+ if needle in text:
891
+ try:
892
+ # 记录绝对路径,避免依赖当前工作目录
893
+ abs_path = str(p.resolve())
894
+ except Exception:
895
+ abs_path = str(p)
896
+ matches.append(abs_path)
897
+
898
+ if not matches:
899
+ return
900
+
901
+ # 在当前工作目录运行 CodeAgent,不进入 crate 目录
902
+ for target_file in matches:
903
+ prompt = "\n".join([
904
+ f"请在文件 {target_file} 中,定位所有 todo!(\"{symbol}\") 占位并替换为对已转换函数的真实调用。",
905
+ "要求:",
906
+ f"- 已转换的目标函数名:{callee_rust_fn}",
907
+ f"- 其所在模块(crate路径提示):{callee_path}",
908
+ f"- 函数签名提示:{callee_rust_sig}",
909
+ f"- 当前 crate 根目录路径:{self.crate_dir.resolve()}",
910
+ "- 你可以使用完全限定路径(如 crate::...::函数(...)),或在文件顶部添加合适的 use;",
911
+ "- 保持最小改动,不要进行与本次修复无关的重构或格式化;",
912
+ "- 如果参数列表暂不明确,可使用合理占位变量,确保编译通过。",
913
+ "",
914
+ f"仅修改 {target_file} 中与 todo!(\"{symbol}\") 相关的代码,其他位置不要改动。",
915
+ "请仅输出补丁,不要输出解释或多余文本。",
916
+ ])
917
+ prev_cwd = os.getcwd()
918
+ try:
919
+ os.chdir(str(self.crate_dir))
920
+ agent = CodeAgent(need_summary=False, non_interactive=True, plan=False, model_group=self.llm_group)
921
+ agent.run(prompt, prefix=f"[c2rust-transpiler][todo-fix:{symbol}]", suffix="")
922
+ finally:
923
+ os.chdir(prev_cwd)
924
+
925
+ def _cargo_build_loop(self) -> bool:
926
+ """在 crate 目录执行 cargo build,失败则使用 CodeAgent 最小化修复,直到通过或达到上限"""
927
+ # 在 crate 目录进行构建与修复循环(切换到 crate 目录执行构建命令)
928
+ workspace_root = str(self.crate_dir)
929
+ i = 0
930
+ while True:
931
+ i += 1
932
+ res = subprocess.run(
933
+ ["cargo", "build", "-q"],
934
+ capture_output=True,
935
+ text=True,
936
+ check=False,
937
+ cwd=workspace_root,
938
+ )
939
+ if res.returncode == 0:
940
+ print("[c2rust-transpiler] Cargo 构建成功。")
941
+ return True
942
+ output = (res.stdout or "") + "\n" + (res.stderr or "")
943
+ print(f"[c2rust-transpiler] Cargo 构建失败 (第 {i} 次尝试)。")
944
+ print(output)
945
+ # 为修复 Agent 提供更多上下文:symbols.jsonl 索引指引 + 最近处理的C源码片段
946
+ symbols_path = str((self.data_dir / "symbols.jsonl").resolve())
947
+ try:
948
+ curr = self.progress.get("current") or {}
949
+ except Exception:
950
+ curr = {}
951
+ sym_name = str(curr.get("qualified_name") or curr.get("name") or "")
952
+ src_loc = f"{curr.get('file')}:{curr.get('start_line')}-{curr.get('end_line')}" if curr else ""
953
+ c_code = ""
954
+ try:
955
+ cf = curr.get("file")
956
+ s = int(curr.get("start_line") or 0)
957
+ e = int(curr.get("end_line") or 0)
958
+ if cf and s:
959
+ p = Path(cf)
960
+ if not p.is_absolute():
961
+ p = (self.project_root / p).resolve()
962
+ if p.exists():
963
+ lines = p.read_text(encoding="utf-8", errors="replace").splitlines()
964
+ s0 = max(1, s)
965
+ e0 = min(len(lines), max(e, s0))
966
+ c_code = "\n".join(lines[s0 - 1 : e0])
967
+ except Exception:
968
+ c_code = ""
969
+
970
+ repair_prompt = "\n".join([
971
+ "目标:以最小的改动修复问题,使 `cargo build` 命令可以通过。",
972
+ "允许的修复:修正入口/模块声明/依赖;对入口文件与必要mod.rs进行轻微调整;避免大范围改动。",
973
+ "- 保持最小改动,避免与错误无关的重构或格式化;",
974
+ "- 请仅输出补丁,不要输出解释或多余文本。",
975
+ "",
976
+ "最近处理的函数上下文(供参考,优先修复构建错误):",
977
+ f"- 函数:{sym_name}",
978
+ f"- 源位置:{src_loc}",
979
+ f"- 目标模块(progress):{curr.get('module') or ''}",
980
+ f"- 建议签名(progress):{curr.get('rust_signature') or ''}",
981
+ "",
982
+ "原始C函数源码片段(只读参考):",
983
+ "<C_SOURCE>",
984
+ c_code,
985
+ "</C_SOURCE>",
986
+ "",
987
+ "如需定位或交叉验证 C 符号位置,可在以下索引中检索:",
988
+ f"- 符号索引文件: {symbols_path}",
989
+ f"- 示例命令: grep -n '\\\"name\\\": \\\"{sym_name}\\\"' '{symbols_path}' || grep -n '\\\"qualified_name\\\": \\\"{sym_name}\\\"' '{symbols_path}'",
990
+ "",
991
+ "上下文:",
992
+ f"- crate 根目录路径: {self.crate_dir.resolve()}",
993
+ f"- 包名称(用于 cargo build -p): {self.crate_dir.name}",
994
+ "",
995
+ "请阅读以下构建错误并进行必要修复:",
996
+ "<BUILD_ERROR>",
997
+ output,
998
+ "</BUILD_ERROR>",
999
+ "修复后请再次执行 `cargo build` 进行验证。",
1000
+ ])
1001
+ prev_cwd = os.getcwd()
1002
+ try:
1003
+ os.chdir(str(self.crate_dir))
1004
+ agent = CodeAgent(need_summary=False, non_interactive=True, plan=False, model_group=self.llm_group)
1005
+ agent.run(repair_prompt, prefix=f"[c2rust-transpiler][build-fix iter={i}]", suffix="")
1006
+ finally:
1007
+ os.chdir(prev_cwd)
1008
+
1009
+ def _review_and_optimize(self, rec: FnRecord, module: str, rust_sig: str) -> None:
1010
+ """
1011
+ 审查生成的实现;若 summary 报告问题,则调用 CodeAgent 进行优化,直到无问题或次数用尽。
1012
+ 审查只关注本次函数与相关最小上下文,避免全局重构。
1013
+ """
1014
+ def build_review_prompts() -> Tuple[str, str, str]:
1015
+ sys_p = (
1016
+ "你是Rust代码审查专家。验收标准:只要主要功能实现且主路径行为与预期一致即可判定为合格;允许忽略异常路径、边界条件和非关键性处理差异(如日志、错误信息格式、轻微风格或非功能性调整)。"
1017
+ "关注点:仅检查主路径(happy path)上的核心输入输出与状态变化是否正确;边界条件、异常路径、资源释放细节等不作为否决项,除非直接影响主路径功能。"
1018
+ "不考虑安全、性能、风格等其他方面。仅在总结阶段输出审查结论。"
1019
+ "禁止尝试修复或修改任何代码;不要输出补丁或提出具体改动方案;仅进行审查并在总结阶段给出结论。"
1020
+ )
1021
+ # 附加原始C函数源码片段,供审查作为只读参考
1022
+ c_code = self._read_source_span(rec) or ""
1023
+ # 附加被引用符号上下文与库替代上下文,以及crate目录结构,提供更完整审查背景
1024
+ callees_ctx = self._collect_callees_context(rec)
1025
+ librep_ctx = rec.lib_replacement if isinstance(rec.lib_replacement, dict) else None
1026
+ crate_tree = _dir_tree(self.crate_dir)
1027
+ usr_p = "\n".join([
1028
+ f"待审查函数:{rec.qname or rec.name}",
1029
+ f"建议签名:{rust_sig}",
1030
+ f"目标模块:{module}",
1031
+ f"crate根目录路径:{self.crate_dir.resolve()}",
1032
+ f"源文件位置:{rec.file}:{rec.start_line}-{rec.end_line}",
1033
+ "",
1034
+ "原始C函数源码片段(只读参考,不要修改C代码):",
1035
+ "<C_SOURCE>",
1036
+ c_code,
1037
+ "</C_SOURCE>",
1038
+ "",
1039
+ "被引用符号上下文(如已转译则包含Rust模块信息):",
1040
+ json.dumps(callees_ctx, ensure_ascii=False, indent=2),
1041
+ "",
1042
+ "库替代上下文(若存在):",
1043
+ json.dumps(librep_ctx, ensure_ascii=False, indent=2),
1044
+ "",
1045
+ "当前crate目录结构(部分):",
1046
+ "<CRATE_TREE>",
1047
+ crate_tree,
1048
+ "</CRATE_TREE>",
1049
+ "",
1050
+ "如需定位或交叉验证 C 符号位置,可在以下索引中检索:",
1051
+ f"- 符号索引文件: {(self.data_dir / 'symbols.jsonl').resolve()}",
1052
+ f"- 示例命令: grep -n '\\\"name\\\": \\\"{rec.qname or rec.name}\\\"' '{(self.data_dir / 'symbols.jsonl').resolve()}' || grep -n '\\\"qualified_name\\\": \\\"{rec.qname or rec.name}\\\"' '{(self.data_dir / 'symbols.jsonl').resolve()}'",
1053
+ "",
1054
+ "请阅读crate中该函数的当前实现(你可以在上述crate根路径下自行读取必要上下文),并准备总结。",
1055
+ ])
1056
+ sum_p = (
1057
+ "请仅输出一个 <SUMMARY> 块,内容为纯文本:\n"
1058
+ "- 若满足“关键逻辑一致”,请输出:OK\n"
1059
+ "- 前置条件:必须在crate中找到该函数的实现(匹配函数名或签名)。若未找到,禁止输出OK,请输出一行:[logic] function not found\n"
1060
+ "- 否则以简要列表形式指出关键逻辑上的问题(每行以 [logic] 开头,避免长文)。\n"
1061
+ "<SUMMARY>...</SUMMARY>\n"
1062
+ "不要在 <SUMMARY> 块外输出任何内容。"
1063
+ )
1064
+ return sys_p, usr_p, sum_p
1065
+
1066
+ i = 0
1067
+ while True:
1068
+ sys_p, usr_p, sum_p = build_review_prompts()
1069
+ agent = Agent(
1070
+ system_prompt=sys_p,
1071
+ name="C2Rust-Review-Agent",
1072
+ model_group=self.llm_group,
1073
+ summary_prompt=sum_p,
1074
+ need_summary=True,
1075
+ auto_complete=True,
1076
+ use_tools=["execute_script", "read_code", "retrieve_memory", "save_memory", "read_symbols"],
1077
+ plan=False,
1078
+ non_interactive=True,
1079
+ use_methodology=False,
1080
+ use_analysis=False,
1081
+ disable_file_edit=True,
1082
+ )
1083
+ prev_cwd = os.getcwd()
1084
+ try:
1085
+ os.chdir(str(self.crate_dir))
1086
+ summary = str(agent.run(usr_p) or "")
1087
+ finally:
1088
+ os.chdir(prev_cwd)
1089
+ m = re.search(r"<SUMMARY>([\s\S]*?)</SUMMARY>", summary, flags=re.IGNORECASE)
1090
+ content = (m.group(1).strip() if m else summary.strip()).upper()
1091
+ if content == "OK":
1092
+ print("[c2rust-transpiler] 代码审查通过。")
1093
+ return
1094
+ # 需要优化:提供详细上下文背景,并明确审查意见仅针对 Rust crate,不修改 C 源码
1095
+ crate_tree = _dir_tree(self.crate_dir)
1096
+ fix_prompt = "\n".join([
1097
+ "请根据以下审查结论对目标函数进行最小优化(保留结构与意图,不进行大范围重构):",
1098
+ "<REVIEW>",
1099
+ content,
1100
+ "</REVIEW>",
1101
+ "",
1102
+ "上下文背景信息:",
1103
+ f"- crate_dir: {self.crate_dir.resolve()}",
1104
+ f"- 目标模块文件: {module}",
1105
+ f"- 建议/当前 Rust 签名: {rust_sig}",
1106
+ "crate 目录结构(部分):",
1107
+ crate_tree,
1108
+ "",
1109
+ "约束与范围:",
1110
+ "- 本次审查意见仅针对 Rust crate 的代码与配置;不要修改任何 C/C++ 源文件(*.c、*.h 等)。",
1111
+ "- 仅允许在 crate_dir 下进行最小必要修改(Cargo.toml、src/**/*.rs);不要改动其他目录。",
1112
+ "- 保持最小改动,避免与问题无关的重构或格式化。",
1113
+ "",
1114
+ "请仅以补丁形式输出修改,避免冗余解释。",
1115
+ ])
1116
+ # 在当前工作目录运行 CodeAgent,不进入 crate 目录
1117
+ ca = CodeAgent(need_summary=False, non_interactive=True, plan=False, model_group=self.llm_group)
1118
+ prev_cwd = os.getcwd()
1119
+ try:
1120
+ os.chdir(str(self.crate_dir))
1121
+ ca.run(fix_prompt, prefix=f"[c2rust-transpiler][review-fix iter={i}]", suffix="")
1122
+ # 优化后进行一次构建验证;若未通过则进入构建修复循环,直到通过为止
1123
+ self._cargo_build_loop()
1124
+ finally:
1125
+ os.chdir(prev_cwd)
1126
+
1127
+ def _mark_converted(self, rec: FnRecord, module: str, rust_sig: str) -> None:
1128
+ """记录映射:C 符号 -> Rust 符号与模块路径(JSONL,每行一条,支持重载/同名)"""
1129
+ rust_symbol = ""
1130
+ # 从签名中提取函数名(简单启发:pub fn name(...)
1131
+ m = re.search(r"\bfn\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(", rust_sig)
1132
+ if m:
1133
+ rust_symbol = m.group(1)
1134
+ # 写入 JSONL 映射(带源位置,用于区分同名符号)
1135
+ self.symbol_map.add(rec, module, rust_symbol or (rec.name or f"fn_{rec.id}"))
1136
+
1137
+ # 更新进度:已转换集合
1138
+ converted = self.progress.get("converted") or []
1139
+ if rec.id not in converted:
1140
+ converted.append(rec.id)
1141
+ self.progress["converted"] = converted
1142
+ self.progress["current"] = None
1143
+ self._save_progress()
1144
+
1145
+ def transpile(self) -> None:
1146
+ """主流程"""
1147
+ order_path = _ensure_order_file(self.project_root)
1148
+ steps = _iter_order_steps(order_path)
1149
+ if not steps:
1150
+ typer.secho("[c2rust-transpiler] 未找到翻译步骤。", fg=typer.colors.YELLOW)
1151
+ return
1152
+
1153
+ # 构建自包含 order 索引(id -> FnRecord,name/qname -> id)
1154
+ self._load_order_index(order_path)
1155
+
1156
+ # 扁平化顺序,按单个函数处理(保持原有顺序)
1157
+ seq: List[int] = []
1158
+ for grp in steps:
1159
+ seq.extend(grp)
1160
+
1161
+ # 若支持 resume,则跳过 progress['converted'] 中已完成的
1162
+ done: Set[int] = set(self.progress.get("converted") or [])
1163
+
1164
+ for fid in seq:
1165
+ if fid in done:
1166
+ continue
1167
+ rec = self.fn_index_by_id.get(fid)
1168
+ if not rec:
1169
+ continue
1170
+ if self._should_skip(rec):
1171
+ continue
1172
+
1173
+ # 读取C函数源码
1174
+ c_code = self._read_source_span(rec)
1175
+
1176
+ # 1) 规划:模块路径与Rust签名
1177
+ module, rust_sig = self._plan_module_and_signature(rec, c_code)
1178
+
1179
+ # 记录当前进度
1180
+ self._update_progress_current(rec, module, rust_sig)
1181
+
1182
+ # 2) 生成实现
1183
+ unresolved = self._untranslated_callee_symbols(rec)
1184
+ self._codeagent_generate_impl(rec, c_code, module, rust_sig, unresolved)
1185
+
1186
+ # 3) 构建与修复
1187
+ ok = self._cargo_build_loop()
1188
+ if not ok:
1189
+ typer.secho("[c2rust-transpiler] 在重试次数限制内未能成功构建,已停止。", fg=typer.colors.RED)
1190
+ # 保留当前状态,便于下次 resume
1191
+ return
1192
+
1193
+ # 4) 审查与优化
1194
+ self._review_and_optimize(rec, module, rust_sig)
1195
+
1196
+ # 5) 标记已转换与映射记录(JSONL)
1197
+ self._mark_converted(rec, module, rust_sig)
1198
+
1199
+ # 6) 若此前有其它函数因依赖当前符号而在源码中放置了 todo!("<symbol>"),则立即回头消除
1200
+ current_rust_fn = self._extract_rust_fn_name_from_sig(rust_sig)
1201
+ # 优先使用限定名匹配,其次使用简单名匹配
1202
+ for sym in [rec.qname, rec.name]:
1203
+ if sym:
1204
+ self._resolve_pending_todos_for_symbol(sym, module, current_rust_fn, rust_sig)
1205
+ # 尝试一次构建以验证修复
1206
+ self._cargo_build_loop()
1207
+
1208
+ typer.secho("[c2rust-transpiler] 所有符合条件的函数均已处理完毕。", fg=typer.colors.GREEN)
1209
+
1210
+
1211
+ def run_transpile(
1212
+ project_root: Union[str, Path] = ".",
1213
+ crate_dir: Optional[Union[str, Path]] = None,
1214
+ llm_group: Optional[str] = None,
1215
+ max_retries: int = 0,
1216
+ resume: bool = True,
1217
+ only: Optional[List[str]] = None,
1218
+ ) -> None:
1219
+ """
1220
+ 入口函数:执行转译流程
1221
+ - project_root: 项目根目录(包含 .jarvis/c2rust/symbols.jsonl)
1222
+ - crate_dir: Rust crate 根目录;默认遵循 "<parent>/<cwd_name>_rs"(与当前目录同级,若 project_root 为 ".")
1223
+ - llm_group: 指定 LLM 模型组
1224
+ - max_retries: 构建与审查迭代的最大次数
1225
+ - resume: 是否启用断点续跑
1226
+ - only: 仅转译给定列表中的函数(函数名或限定名)
1227
+ """
1228
+ t = Transpiler(
1229
+ project_root=project_root,
1230
+ crate_dir=crate_dir,
1231
+ llm_group=llm_group,
1232
+ max_retries=max_retries,
1233
+ resume=resume,
1234
+ only=only,
1235
+ )
1236
+ t.transpile()