agentleFS
po4ykapo4yka/rust-skills/skills/rust-port/SKILL.md

rust-port

Skill5 starsChanged 11 days agoBSD-3-ClauseView on GitHub

Use when porting or rewriting a C, C++, Python, Go, JavaScript, or TypeScript module in Rust that must keep the original's output. Covers the behavior inventory, tests ported first, the traps that change output (floor division, rounding, string length, ordered maps, strict parsing), a recorded comparison with the original, and PORTING.md. Not for calling a C library from Rust (use rust-native-linking).

7 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use rust-best-practices.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • MicrosoftSkill

    rust-best-practices

    Guide for writing idiomatic Rust code based on Apollo GraphQL's best practices handbook. Use this skill when: (1) writing new Rust code or functions, (2) reviewing or refactoring existing Rust code, (3) deciding between borrowing vs cloning or ownership patterns, (4) implementing error handling with Result types, (5) optimizing Rust code for performance, (6) writing tests or documentation for Rust projects.

    Also in 4 projects that use this

The file

We split this skill into the rules it gives an agent

12 rules. 0 are common, 6 are distinctive, and 6 are very specific to this project.

  • 0

    Common

    Most of the 7 edited versions we read keep these.

  • 6

    Distinctive

    This author's own choices. Worth a look.

  • 6

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Inventory the behavior: Read the original, its tests, and its callers.

    line 16
  2. Distinctive:

    Port the tests first: Write one Rust test for each original test, and give it a name that maps back to the original.

    line 20
  3. Distinctive:

    Implement against the ported tests: Keep the original algorithm unless it is wrong for the input it gets.

    line 28
  4. Distinctive:

    If the original has a bug or an ambiguity, keep its behavior, record it, and let the owner decide.

    line 33
  5. Distinctive:

    Run these when a Rust toolchain is present, and fix every finding before you report:

    line 38
  6. Distinctive:

    Run the original once on a corpus, store each input with its output, and make a Rust test replay them.

    line 54
  7. Very specific:

    Run the original on each input, and record the exact command and the original's version in PORTING.md.

    line 60
  8. Very specific:

    Read references/source-languages.md when you map the constructs of one source language: memory and error handling of C and C++, Python …

    line 144
  9. Very specific:

    Do not grow the API. Port the original's public surface, plus the standard traits Rust callers expect: Debug, Clone, PartialEq, Display and …

    line 154
  10. Very specific:

    Do not port the source runtime.

    line 157
  11. Very specific:

    Do not write unsafe or raw pointers unless the port must also expose or call a C ABI.

    line 159
  12. Very specific:

    Write PORTING.md next to the code.

    line 165

“Common” is measured: kept by at least 6 in 10 of the 7 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Rust Port

A port succeeds when Rust callers get the same results as the original's callers, for the inputs that the original receives. A line-by-line transliteration compiles, keeps every accident of the source language, and adds the borrow checker on top. Port the behavior. Design the Rust shape from that behavior.

The procedure

  1. Inventory the behavior. Read the original, its tests, and its callers. List each public function, its inputs, its outputs, and each way it fails. Add the quiet behavior, because a port goes wrong there: output order, tie breaks, empty input, whitespace and case handling, integer limits, rounding, the length of non-ASCII text, and what counts as an error.
  2. Port the tests first. Write one Rust test for each original test, and give it a name that maps back to the original. Run them; they fail until the port exists. When the original has no tests, record its outputs on the inventory inputs first (A recorded comparison), and make those the Rust tests: a test written from a guess about the original proves nothing.
  3. Design the Rust API from the inventory, not from the original's shape. Drop what exists only because of the source language: out-parameters, NULL returns, error codes, global state, fixed buffers, classes that only group functions, and type checks at run time.
  4. Implement against the ported tests. Keep the original algorithm unless it is wrong for the input it gets; record a change of algorithm in the notes.
  5. Compare with the original on recorded inputs (Verification).
  6. Write PORTING.md (The porting notes).

If the original has a bug or an ambiguity, keep its behavior, record it, and let the owner decide. A port is not the place for a silent fix.

Verification

Run these when a Rust toolchain is present, and fix every finding before you report:

cargo test --locked
cargo clippy --all-targets --locked -- -D warnings
ClaimCheckA pass does not prove
The port matches the original's testsEvery ported test passes; the count equals the original'sBehavior that no original test covers
The port matches the original on real inputA recorded comparison (below)Inputs outside the recorded corpus
The port matches a reference that a test can call in-process (a C library, an earlier Rust version)The differential fuzz target of the rust-test-tools skillInputs that the fuzzer did not reach
Output order and formatting matchA byte comparison of whole outputs, not a parsed comparisonLocale or terminal effects of the original

A recorded comparison

Run the original once on a corpus, store each input with its output, and make a Rust test replay them. This works for any source language, including one that the Rust test cannot call:

  1. Collect 30 to 100 inputs: real ones from logs or fixtures, plus the edge cases of the inventory. Add non-ASCII text, empty input, the largest and smallest numbers, and negative numbers for every division or modulo.
  2. Run the original on each input, and record the exact command and the original's version in PORTING.md.
  3. Store each input and output as a golden file pair, and add one test that runs the Rust entry point on each input and compares bytes. The rust-test-tools skill owns the golden layout, the -text attribute that keeps line endings exact, and the scrubbing of volatile fields.
  4. Regenerate an output file only by running the original again, never from the port: a blessed Rust output is no longer an oracle.

Traps that change output

Each row is a difference that a test written from the Rust side does not catch.

Symptom in the portCauseFix
A negative result is off by one against PythonRust / and % truncate toward zero; Python // and % floorA floor helper (below). div_euclid matches Python only for a positive divisor
A .5 value rounds the other wayf64::round rounds half away from zero; Python round() rounds half to even; JavaScript Math.round rounds half upround_ties_even for Python (Rust 1.77+); for JavaScript, ceil at an exact negative half and round otherwise (the probe below). (x + 0.5).floor() is wrong near 0.5 and above 2^52
A count or a column differs on non-ASCII textlen() counts UTF-8 bytes in Rust and Go, code points in Python, UTF-16 units in JavaScriptchars().count() for Python, encode_utf16().count() for JavaScript; never slice a &str at a Python or JavaScript index
Output lines come in a different orderPython dict and JavaScript Map keep insertion order; HashMap does notA Vec of pairs or indexmap::IndexMap when order is observable; BTreeMap only when the original sorts
Input that the original accepts is rejectedstr::parse rejects surrounding whitespace and _; Python int() accepts both, C strtol skips leading space and stops at the first bad characterTrim, or write the original's rule out in one function and test it
A line count differsstr::lines splits on \n and \r\n only; Python splitlines() also splits on a lone \r, \x0b, \x0c, \x1c, \x85, \u2028, and moreSplit on the original's separator set when the input can hold them
Fields appear or vanishPython split() with no argument is split_whitespace; split(' ') keeps empty fieldsMatch the call, not the name
A debug build panics where the original wrappedGo integers and C unsigned integers wrap; Rust panics on overflow in debug buildswrapping_* where the original wraps by contract; checked_* where input can be large; i128 or a big-integer crate for Python int
Equal items swap placessort_unstable* is not stable; Python sorted and JavaScript sort (ES2019+) aresort_by_key or sort_by
JavaScript sorts [10, 9, 1] as [1, 10, 9]The default JavaScript sort compares stringsPort the comparison that the original really used, then record it as a preserved quirk
A large JavaScript integer loses digitsJavaScript numbers are f64, exact only up to 2^53f64 when the original depends on it, i64 when it was always meant as an integer; record which
A sort of floats does not compile, or NaN lands in a new placef64 is not Ordsort_by(f64::total_cmp), and record the NaN position

The Rust-side claims that a probe can show, executed (Rust 1.99.1):

/// Floor division as Python's `//` defines it. `i64::MIN / -1` overflows in Rust, where Python
/// returns 2^63: check that pair when the input can reach it.
fn floor_div(a: i64, b: i64) -> i64 {
    let q = a / b;
    if a % b != 0 && ((a < 0) != (b < 0)) { q - 1 } else { q }
}

/// The remainder that pairs with `floor_div`: it takes the sign of the divisor. It corrects
/// `a % b` instead of computing `a - b * q`, which overflows near the limits.
fn floor_mod(a: i64, b: i64) -> i64 {
    let r = a % b;
    if r != 0 && ((r < 0) != (b < 0)) { r + b } else { r }
}

fn main() {
    assert_eq!((-7 / 2, -7 % 2), (-3, -1));
    assert_eq!((floor_div(-7, 2), floor_mod(-7, 2)), (-4, 1));
    assert_eq!(((-7i64).div_euclid(2), 7i64.div_euclid(-2)), (-4, -3));
    assert_eq!((floor_div(7, -2), floor_mod(7, -2)), (-4, -1));
    assert_eq!((floor_div(i64::MIN, 3), floor_mod(i64::MIN, 3)), (-3074457345618258603, 1));
    assert!(i64::MIN.checked_div(-1).is_none());

    assert_eq!((2.5f64.round(), (-2.5f64).round()), (3.0, -3.0));
    assert_eq!((2.5f64.round_ties_even(), 3.5f64.round_ties_even()), (2.0, 4.0));
    // JavaScript Math.round: half toward positive infinity, exact for every f64.
    let js_round = |x: f64| if x.fract() == -0.5 { x.ceil() } else { x.round() };
    assert_eq!((js_round(-2.5), js_round(2.5), js_round(0.49999999999999994)), (-2.0, 3.0, 0.0));
    assert_eq!(js_round(4503599627370497.0), 4503599627370497.0);
    assert_eq!((0.49999999999999994f64 + 0.5).floor(), 1.0); // the polyfill fails here

    let s = "héllo😀";
    assert_eq!((s.len(), s.chars().count(), s.encode_utf16().count()), (10, 6, 7));

    assert!("  42 ".parse::<i32>().is_err());
    assert!("1_000".parse::<i32>().is_err());
    assert_eq!("+5".parse::<i32>(), Ok(5));

    assert_eq!("a\u{2028}b\x0cc".lines().count(), 1);
    assert_eq!(" a  b ".split_whitespace().collect::<Vec<_>>(), ["a", "b"]);
    assert_eq!(" a  b ".split(' ').collect::<Vec<_>>(), ["", "a", "", "b", ""]);

    assert_eq!(i32::MAX.wrapping_add(1), i32::MIN);
    assert_eq!(i32::MAX.checked_add(1), None);

    let mut v = [(1, 'b'), (0, 'x'), (1, 'a')];
    v.sort_by_key(|&(k, _)| k);
    assert_eq!(v, [(0, 'x'), (1, 'b'), (1, 'a')]);
    let mut f = [2.0, f64::NAN, 1.0];
    f.sort_by(f64::total_cmp);
    assert_eq!(&f[..2], &[1.0, 2.0]);
    assert!(f[2].is_nan());
}

Read references/source-languages.md when you map the constructs of one source language: memory and error handling of C and C++, Python classes and exceptions, Go interfaces and goroutines, and JavaScript null, undefined, and promises.

Keep the port in proportion

  • Port the inputs that the original receives. Where Rust's standard library differs from the source runtime only on exotic input (Unicode digits, non-ASCII whitespace, unbounded integers), record the difference in PORTING.md. Write a compatibility shim only when the owner says such input occurs.
  • Do not grow the API. Port the original's public surface, plus the standard traits Rust callers expect: Debug, Clone, PartialEq, Display and std::error::Error on the error type, FromStr for parsed text, and Default where the original has defaults.
  • Do not port the source runtime. Use str::trim, str::parse, and char methods; do not rewrite Python's strip, int(), or repr(), or C's locale-dependent isspace.
  • Do not write unsafe or raw pointers unless the port must also expose or call a C ABI. When the original is a C library that stays in use, bind it instead of porting it: a -sys crate with build.rs and bindgen (the rust-native-linking skill owns this, when it is installed).

The porting notes

Write PORTING.md next to the code. A reviewer checks it in a few minutes:

# Porting notes: <original name and version or commit>

## API map
| Original | Rust |

## Test map
| Original test | Rust test |

## Deliberate differences
<each behavior that now differs, the input that shows it, and why>

## Preserved quirks
<odd behavior kept on purpose, with the test that pins it>

## Verification
<the commands run and their results; the recorded-comparison command and corpus size;
 the checks that were not run, and why>

A difference that is not in PORTING.md is a bug in the port.

Related skills

Each applies when it is installed.

SkillBoundary
rust-tddCharacterization tests for an original that has none, and the red-green cycle
rust-test-toolsThe differential fuzz target, golden-file mechanics, and property tests
rust-panic-safetyA port that must keep a C ABI: no panic may cross it
rust-native-linkingBinding a C library that stays, instead of porting it (build.rs, bindgen, -sys crates)
rust-serdeSerialized output that must match the original byte for byte (serde_json formatting, key order)
rust-cliA ported command line tool: argument parsing, exit codes, and the stdout and stderr contract (clap)

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.