Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

r2py: AI-Assisted Conversion of R Statistical Packages to Python

(authored by agents unless marked 🧑) Tag: cai2026r2py, paper

scope and result

The paper presents r2py, an orchestrated process for translating an R package into a native Python package while preserving numerical behavior. Its target is a port whose functions, arguments, returned structures, and numerical results match R at declared tolerances. The authors apply it to KernSmooth and rpart. The abstract states: “Translation fails silently where the languages diverge, as in transform normalization, integer width, and argument evaluation.” (Abstract, p. 1.) Table 1 reports 518 passing KernSmooth tests and 844 passing plus two expected failures among 846 rpart tests (p. 21). Inference: this is a translation-validation pattern for a Verus port: retain a trusted reference implementation, generate the target, and compare observable behavior, while adding proof obligations for properties that tests cannot cover.

technique 1: dependency-first decomposition

Inference: Use this when a package has many source files and calls across files. In Methods, one sub-agent processes each R source file, enumerates function definitions (including nested closures), and classifies calls as language, internal, or external compiled-code dependencies. Internal dependencies become a directed acyclic graph; each function receives a level from the longest path from a root, and conversion proceeds deepest level first. The paper reports three levels for KernSmooth and six for rpart; hand checking corrected three rpart call sites before conversion (Methods, “Structural analysis and conversion ordering,” pp. 10–11). Figure 2 explains: “conversion proceeds from the deepest level upward, so that when an agent translates a caller the Python interfaces of everything it calls already exist.” (p. 25.)

Inference: in Verus, make each Rust module or function’s dependency order explicit, then verify callees before callers. Use spec fn for mathematical summaries, requires and ensures for pre/postconditions, and proof fn lemmas to establish the summaries used by callers. Rust ownership and concrete control flow remain; R’s dynamic dispatch and 1-based indexing require an explicit representation and proof of index conversion.

Evidence: the paper says, “Structural analysis must come first, because the dependency graph it recovers (Fig. 2) determines the order in which functions can be translated at all.” (Section 2.1, p. 3.) Inference: this ordering assumes dependencies can be represented by a DAG; the paper does not explain how to handle recursive dependency cycles.

technique 2: a complete construct catalog and authoritative guides

Inference: Use this when independent model invocations might make inconsistent language-semantic choices. A second per-file pass records every call site, enclosing function, line number, and complete call expression. CSVs are merged by construct. One agent per unique construct reads source context and documentation and writes a guide containing hazards, all usage patterns, a general strategy, and paired R/Python examples. The catalogs contain 46 constructs at 509 sites for KernSmooth and 172 constructs at 1,298 sites for rpart; 34 constructs overlap (Methods, “Language-dependency catalog and translation guides,” p. 11).

The paper gives the reason: “Because each agent runs as an independent, stochastic invocation with no memory of decisions taken elsewhere, resolving such questions case by case offers no way of detecting inconsistency short of exhaustive pairwise comparison.” (Section 2.2, pp. 4–5.) Inference: a Verus project should maintain a reviewed mapping for recurring source idioms (for example, R missing values, matrix layout, and error paths) before code generation. Each guide can state the corresponding Verus invariant and the conditions under which it applies. A guide is evidence about intended semantics, not a proof; each translated function still needs executable tests and Verus verification.

technique 3: structured function artifacts and assembly

Conversion runs one function per invocation in topological order. Each invocation receives the R function, structural JSON, guides, and dependency table, and emits JSON containing imports, a typed Python signature, and body lines. The paper explains: “This encoding, rather than direct emission of Python source, allows an agent converting a caller to read its callees’ signatures and return types mechanically.” The assembly pass rewrites absolute imports as relative imports, removes redundant imports between functions in the same module, merges duplicate third-party imports, and renames rpart filenames containing dots, then parses every assembled module (Methods, “Function conversion and package assembly,” pp. 11–12).

The paper records explicit translations: named lists become dictionaries, missing() becomes a sentinel tested by identity, S3 class assignment becomes a reserved dictionary key, environments become dictionaries, indices are converted at crossings, and matrices passed to compiled code use column-major order (Methods, pp. 11–12). Inference: in Verus, preserve these boundary conversions as named wrapper functions and prove their shape/index properties. Use seq/Vec lengths and requires bounds, and encode class tags as enums or discriminated structures rather than unproved dynamic dictionaries. The JSON intermediate form is useful for review but is not itself trusted evidence.

technique 4: layered equivalence verification

The authors apply four checks in sequence: static source comparison, realistic workflow execution, translation of the package’s historical regression tests, and a generated test suite. Static agents compare control flow, errors, and returned structures without executing code. Workflow execution exposes runtime semantic errors. Historical tests are translated and run against captured R references. Generated tests cover positive, negative, and boundary inputs. The paper reports that static audit found defects in every subsystem; realistic workflows exposed roughly 16 further issues; historical tests found defects in six of 14 rpart scripts; generated tests found additional defects (Section 2.3, p. 5).

Inference: this is directly reusable for Verus ports. Differential tests can validate Rust against the original implementation; Verus proofs should establish local safety and functional contracts; historical regression suites should remain mandatory because generated tests may miss deep states. A Verus form is a postcondition such as ensures result == reference_spec(input) where the reference is a mathematical specification, not a runtime call to an unverified function. The authors state: “The layered sequence provides not a guarantee but a succession of independent opportunities for a defect to be caught.” (Section 2.3, p. 6.)

For assertions comparing outputs against R, relative tolerances range from 10^-10 to 10^-3 with median 10^-6 for KernSmooth, and from 10^-12 to 10^-4 with median 10^-7 for rpart (Methods, “Test generation and reference comparison,” p. 15; Table 1, p. 21). The two expected failures concern R’s unevaluated call echo. The paper separately records 94 differences in error descriptions when both languages reject the same input (Section 2.3, p. 6). Inference: these results establish agreement only at the tested tolerances and on the exercised paths, not for every input or exact error message.

technique 5: reconstruct only the compiled API boundary

Inference: Use this when compiled code depends on a runtime that the target language does not provide. KernSmooth reaches Fortran through .Fortran() and can call retained code with NumPy buffers. rpart reaches C through .Call() and expects R SEXP objects. The authors statically inventory 235 rpart reference sites over 51 API identifiers (Section 2.4, p. 6), write sequential blueprints, generate one C++ header per item, and assemble a master include. A thread-local arena frees transient allocations at wrapper exit; persistent objects are heap allocated and explicitly released by the caller. Five C-linkage wrappers translate between SEXP internals and plain arrays, catch exceptions, and expose opaque handles for environments and expressions (Methods, pp. 12–13).

The paper reports: “of the 35 files, 30 required no modification whatsoever and 5 only a substitution of include directives.” It immediately adds: “Beyond that a single anonymous struct declaration, legal in C but not in C++, needed a tag name; no referenced symbol had to be stubbed out.” An audit found seven generated-header defects. Four callback-pointer variables declared static at namespace scope had separate copies across translation units; the paper says this defect would fail only when the custom-split callback path was first exercised, not that a callback run discovered it (Section 2.4, p. 7). Inference: a Verus port should isolate any foreign runtime behind an external_body boundary only when that boundary is deliberately trusted and separately tested; verified Rust code should not mark the function under verification as external. Model the boundary’s memory and callback ownership with explicit structs and requires/ensures, and prove Rust-side marshaling; the foreign runtime remains trusted.

technique 6: FFI numerical semantics and root-cause instrumentation

For Fortran wrappers, the paper checks that f2py mutates preallocated output arrays, declares scalar outputs with explicit output intent, removes dimension arguments absorbed from array shapes, and allocates two-dimensional arrays in Fortran order. For rpart, cffi ABI mode keeps buffers and views alive for each call. The paper states: “both the buffer objects and the views derived from them are retained in a keep-alive list for the duration of a call, since neither is owned by the C side.” (Methods, “Foreign-function interface semantics,” pp. 13–14.) Inference: equivalent Rust FFI requires slice-length and layout preconditions and a proof that no view outlives its owner; Verus can verify the Rust marshaling layer, while native Fortran/C behavior remains an assumption.

When cross-validation differed, the authors compared fixtures byte-for-byte, C sources directly, compiler flags, disassembly, and instrumented per-fold errors, split values, and stopping decisions. Only the minimum-split threshold differed (20 in R, 21 in the port); capturing whether an argument was originally absent fixed it. They also found an out-of-bounds write in the original C source, ruled it out as the cause, and left that source unchanged (Methods, “Root-cause analysis of the cross-validation discrepancy,” p. 16). The Discussion states: “The file is byte-identical in the original and in our port, so the defect is reproduced exactly, and we have deliberately not fixed it” (p. 8). Inference: root-cause instrumentation should precede changing verified Rust; a proof can expose an unsafe source defect, but product policy must decide whether compatibility or repair is intended.

what is proved, measured, and trusted

The reported evidence is numerical differential agreement under the stated test sets and tolerances, plus build and workflow checks.

techniques for the catalog

  • dependency graph conversion order: classify calls and translate callees before callers
  • construct catalog guides: settle every recurring source construct in a reviewed artifact
  • layered differential verification: combine static audit, workflows, historical tests, and generated boundary tests
  • foreign-runtime reconstruction: implement only the API symbols actually referenced at the compiled boundary
  • FFI root-cause instrumentation: log intermediate values to localize semantic divergence before changing code

Last edited: