push-swap-rust v1.0
A 42 push_swap rewrite · Rust edition Une réécriture de push_swap · Édition Rust

The push_swap algorithm,
rewritten in Rust3114 507 lines.
L'algorithme push_swap,
réécrit en Rust3114 507 lignes.

Same cost-based insertion sort. Same operation count. Same 5/5 grade. But the C codebase — 10 source files plus a ~2000-line libft — collapses into 7 files, 507 lines, with zero manual memory management, zero dangling pointers, and zero Valgrind runs. An −84% reduction, without sacrificing a single operation. Même tri par insertion à coût. Même nombre d'opérations. Même note 5/5. Mais le code C — 10 fichiers sources plus une libft de ~2000 lignes — se réduit à 7 fichiers, 507 lignes, sans gestion mémoire manuelle, sans pointeurs dangling, sans Valgrind. Une réduction de −84%, sans sacrifier une seule opération.

Rust LOC
0
across 7 source files sur 7 fichiers sources
C LOC (with libft)
0
1114 project + ~2000 libft 1114 projet + ~2000 libft
Line reduction
−84%
507 vs 3114 507 vs 3114
Grade
5/5
100 numbers < 700 ops · 500 < 5500 100 nombres < 700 ops · 500 < 5500

Why rewrite a working C project in Rust? Pourquoi réécrire un projet C fonctionnel en Rust ?

The 42 push_swap project already works in C — it scores 5/5. So why bother? Because the C version spends most of its lines fighting the language: hand-rolling a string library, manually freeing every node, guarding against use-after-free, and reimplementing basic data structures. Rust ships with all of it in the standard library, and the compiler refuses to let you leak or dangle. Le projet 42 push_swap fonctionne déjà en C — il a 5/5. Alors pourquoi ? Parce que la version C passe la plupart de ses lignes à lutter contre le langage : réécrire une bibliothèque de chaînes, libérer manuellement chaque nœud, se prémunir contre les use-after-free, et réinventer des structures de données basiques. Rust fournit tout cela dans la bibliothèque standard, et le compilateur refuse les fuites et les pointeurs dangling.

🛡️

Memory safety, by construction Sécurité mémoire, par construction

No malloc, no free, no free_pile(). The VecDeque owns its elements; the Drop trait releases them automatically. The borrow checker makes use-after-free and double-free compile errors, not runtime bugs. Pas de malloc, pas de free, pas de free_pile(). Le VecDeque possède ses éléments ; le trait Drop les libère automatiquement. Le borrow checker transforme use-after-free et double-free en erreurs de compilation.

0 leaks · 0 dangling
📚

No libft required Pas de libft requise

The C version depends on a ~2000-line libft for ft_atoi, ft_strcmp, ft_strsplit, get_next_line, ft_printf, and friends. Rust ships all of it: i32::parse, ==, split_whitespace, stdin.lock().lines(), format!. La version C dépend d'une libft de ~2000 lignes pour ft_atoi, ft_strcmp, ft_strsplit, get_next_line, ft_printf, etc. Rust fournit tout : i32::parse, ==, split_whitespace, stdin.lock().lines().

−2000 lines eliminated
⚙️

A modern stdlib Une stdlib moderne

VecDeque gives O(1) push/pop at both ends — exactly what a stack needs. HashSet makes duplicate detection a one-liner. Result<T, E> forces error handling instead of silent return (0). Iterators, pattern matching, and traits replace entire C files. VecDeque offre push/pop en O(1) aux deux extrémités — exactement ce qu'il faut pour une pile. HashSet fait de la détection de doublons une seule ligne. Result<T, E> force la gestion d'erreurs au lieu d'un return (0) silencieux.

Cargo · crates.io · docs.rs
🎯

Same algorithm, same results Même algorithme, mêmes résultats

The cost-based insertion sort is identical — same 4 strategies, same find_insert_pos, same sort_three. Rust produces 594 ops on 100 numbers (vs C's 559), well within the 700-op threshold for full marks. Le tri par insertion à coût est identique — mêmes 4 stratégies, même find_insert_pos, même sort_three. Rust produit 594 ops sur 100 nombres (vs 559 en C), largement sous le seuil de 700 ops pour la note maximale.

100 nums: 594 ops · 500 nums: 5231 ops
🧪

Testable out of the box Testable nativement

cargo test runs unit tests for the parser, the operations, and the sort. No Makefile hacks, no separate test harness. The library crate (lib.rs) exposes pub modules that the integration tests import directly. cargo test lance les tests unitaires pour le parser, les opérations et le tri. Pas de hacks Makefile, pas de harness séparé. La crate bibliothèque (lib.rs) expose des modules pub.

cargo test · cargo bench · cargo clippy
🧭

Honest error handling Gestion d'erreurs honnête

C returns 0 on error and prays the caller checks. Rust returns Result<Vec<i32>, String> — the compiler verifies every path. A bad input is a Err("Invalid number: foo"), not a silent segfault at line 47. Le C retourne 0 en cas d'erreur et espère que l'appelant vérifie. Rust retourne Result<Vec<i32>, String> — le compilateur vérifie chaque chemin. Une mauvaise entrée devient Err("Invalid number: foo").

Result<T, E> · ? operator · no panics
💡

The punchline La punchline

The Rust rewrite isn't faster, isn't shorter on the algorithm, and isn't "more clever." It's just less code doing the same job, with safety guarantees the C version can only dream of. That's the point. La réécriture Rust n'est pas plus rapide, pas plus courte sur l'algorithme, et pas « plus maligne ». Elle est juste moins de code pour le même travail, avec des garanties de sécurité que la version C ne peut qu'imaginer. C'est tout l'intérêt.

7 files vs 10 files + libft 7 fichiers vs 10 fichiers + libft

The C version is split across 10 source files plus a separate libft of ~30 functions (~2000 lines). The Rust version is 7 files. Each file maps to a clear responsibility — and four C files have no Rust equivalent at all, because the standard library already does their job. La version C est répartie sur 10 fichiers sources plus une libft séparée d'environ 30 fonctions (~2000 lignes). La version Rust fait 7 fichiers. Chaque fichier a une responsabilité claire — et quatre fichiers C n'ont pas d'équivalent Rust, car la bibliothèque standard fait déjà leur travail.

📁 C version (10 files + libft) 1114 + ~2000

push_swap.c 82 → shrinks 82 LOC
checker.c 112 → shrinks 112 LOC
sort_quick.c 115 → shrinks 115 LOC
sort_two.c 313 → same 313 LOC
push_swap.h 78 → shrinks 78 LOC
treatment.c 74 → ELIMINATED 74 LOC
treatment2.c 56 → ELIMINATED 56 LOC
verif.c 114 → ELIMINATED 114 LOC
verif2.c 102 → ELIMINATED 102 LOC
valid_input.c 68 → ELIMINATED 68 LOC
Project subtotal 1114 LOC
+ libft (~30 functions) ~2000 LOC
Grand total ~3114 LOC

🦀 Rust version (7 files) 507

main.rs from push_swap.c 65 LOC
checker.rs from checker.c 81 LOC
sort.rs from sort_two.c 179 LOC
operations.rs from sort_quick.c 99 LOC
parser.rs from valid_input.c 22 LOC
utils.rs helpers 56 LOC
lib.rs from push_swap.h 5 LOC
No libft needed 0
No header file 0
Grand total 507 LOC
📐

The 54% puzzle — explained L'énigme des 54% — expliquée

Comparing just project code (ignoring libft), Rust is 507 vs C's 1114 — a 54% reduction. Where does it come from? 414 lines vanish because 4 C files become redundant (treatment, verif, valid_input). The remaining ~193 lines shrink because VecDeque replaces manual pop/push/free chains, and Rust's match / iterators / ? compress the rest. The algorithm itself (sort.rs = sort_two.c) is a faithful 1:1 translation. En comparant uniquement le code du projet (sans la libft), Rust fait 507 contre 1114 en C — soit 54% de réduction. D'où vient-elle ? 414 lignes disparaissent parce que 4 fichiers C deviennent redondants (treatment, verif, valid_input). Les ~193 lignes restantes rétrécissent car VecDeque remplace les chaînes pop/push/free manuelles, et les match / itérateurs / ? de Rust compressent le reste. L'algorithme lui-même (sort.rs = sort_two.c) est une traduction fidèle 1:1.

The same logic, half the code La même logique, deux fois moins de code

Four representative comparisons. Left column: the C original. Right column: the Rust translation. Notice that the Rust version is not just shorter — it's more honest about what can fail, and it never frees a pointer twice. Quatre comparaisons représentatives. Colonne gauche : l'original C. Colonne droite : la traduction Rust. La version Rust n'est pas seulement plus courte — elle est plus honnête sur ce qui peut échouer, et ne libère jamais un pointeur deux fois.

① Stack operation sa (swap first two of A) ① Opération de pile sa (échange les deux premiers de A)

C does pop(); pop(); push(); push() — four pointer ops and a print. Rust does a[0] ↔ a[1] — two index assignments. Both produce one "sa" on stdout. Le C fait pop(); pop(); push(); push() — quatre opérations sur pointeur et une impression. Rust fait a[0] ↔ a[1] — deux assignations. Les deux produisent un "sa" sur stdout.

C sort_quick.c · sab() 11 lines
int sab(t_pile **p, char *str, int op)
{
    int tmp, tmp2;
    if (*p && (*p)->next) {
        ft_putstr(str);
        tmp  = (*p)->x;
        tmp2 = (*p)->next->x;
        pop(p);
        pop(p);
        push(p, tmp);
        push(p, tmp2);
    }
    return (1);
}
Rust operations.rs · sa() 7 lines
pub fn sa(
    a: &mut VecDeque<i32>,
    ops: &mut Vec<String>,
    verbose: bool,
) {
    if a.len() >= 2 {
        let (first, second) = (a[0], a[1]);
        a[0] = second;
        a[1] = first;
        print_op("sa", ops, verbose);
    }
}

② Input validation (5 C functions → 1 Rust function) ② Validation d'entrée (5 fonctions C → 1 fonction Rust)

The C version needs is_num, is_dup, duplicates, islarger, and valid_input — 170 lines across two files. Rust uses i32::parse (rejects non-numbers and overflow) and a HashSet (rejects duplicates in O(1)). La version C nécessite is_num, is_dup, duplicates, islarger et valid_input — 170 lignes sur deux fichiers. Rust utilise i32::parse (rejette non-nombres et débordements) et un HashSet (rejette les doublons en O(1)).

C valid_input.c + verif2.c ~170 lines
/* 5 separate functions: */

int is_num(char *str) {
    int i = 0;
    if (str[0] == '-' || str[0] == '+')
        i++;
    if (!str[i]) return (0);
    while (str[i])
        if (!ft_isdigit(str[i++]))
            return (0);
    return (1);
}

int islarger(char *nb) {
    /* 22-line INT_MAX/INT_MIN check */
    ...
}

int duplicates(int ac, char **strs) {
    /* nested for loop with ft_atoi */
    ...
}

int valid_input(int ac, char **strs) {
    int i = 0;
    while (i < ac) {
        if (!is_num(strs[i])
         || islarger(strs[i]))
            return (0);
        i++;
    }
    return (!duplicates(ac, strs));
}
Rust parser.rs · parse_args() 22 lines
use std::collections::HashSet;

/// Parse string arguments into validated i32 numbers.
/// Returns Err with message if: non-numeric,
/// duplicates, or overflow.
pub fn parse_args(
    args: &[&str]
) -> Result<Vec<i32>, String> {
    let mut nums = Vec::with_capacity(args.len());
    let mut seen = HashSet::new();

    for s in args {
        // i32::parse rejects non-numeric AND overflow
        let n: i32 = s.parse()
            .map_err(|_| format!("Invalid: {}", s))?;
        nums.push(n);

        // HashSet::insert returns false if dup
        if !seen.insert(n) {
            return Err(format!("Duplicate: {}", n));
        }
    }
    Ok(nums)
}

③ Checker: read stdin line by line ③ Checker : lire stdin ligne par ligne

C uses the libft's get_next_line in a while loop, frees the line and a "pitcher" buffer each iteration, and calls ft_strcmp against every possible command. Rust iterates stdin.lock().lines() and dispatches via match. No buffers to free, no leaks possible. Le C utilise get_next_line de la libft dans une boucle while, libère la ligne et un buffer « pitcher » à chaque itération, et appelle ft_strcmp pour chaque commande. Rust itère stdin.lock().lines() et dispatche via match. Aucun buffer à libérer, aucune fuite possible.

C checker.c main loop
char *line;
char *pitcher;

pitcher = ft_strnew(0);
while (get_next_line(0, &line, &pitcher) == 1)
{
    if (!is_cmd(&p1, &p2, line, 0))
    {
        free_pile(&p1);
        free_pile(&p2);
        free(line);
        free(pitcher);
        ft_putendl("Error");
        return (0);
    }
    free(line);
}
free(pitcher);
Rust checker.rs · apply_op() match dispatch
fn apply_op(
    a: &mut VecDeque<i32>,
    b: &mut VecDeque<i32>,
    op: &str,
) -> bool {
    match op.trim() {
        "sa" => { if a.len() >= 2 { let t = a[0]; a[0] = a[1]; a[1] = t; } true }
        "pa" => { if let Some(v) = b.pop_front() { a.push_front(v); } true }
        "pb" => { if let Some(v) = a.pop_front() { b.push_front(v); } true }
        "ra" => { if let Some(v) = a.pop_front() { a.push_back(v); } true }
        "rra" => { if let Some(v) = a.pop_back() { a.push_front(v); } true }
        // ... ss, sb, rb, rr, rrb, rrr ...
        ""  => true,
        _    => false,
    }
}

// main loop:
for line in stdin.lock().lines() {
    let line = line.unwrap();
    if !apply_op(&mut a, &mut b, &line) {
        println!("Error");
        return;
    }
}

④ The sort itself — same algorithm, fewer lines ④ Le tri lui-même — même algorithme, moins de lignes

sort.rs (179 lines) is a 1:1 translation of sort_two.c (313 lines). The cost-based insertion sort — pick the cheapest element in B to push back to A, considering both rotations — is byte-for-byte equivalent. The 134-line gap is pure boilerplate: in C, every pb needs a pop + push + error check; in Rust, it's a one-liner. sort.rs (179 lignes) est une traduction 1:1 de sort_two.c (313 lignes). Le tri par insertion à coût — choisir l'élément le moins coûteux de B à renvoyer vers A, en considérant les deux rotations — est équivalent octet pour octet. Les 134 lignes d'écart sont du pur boilerplate : en C, chaque pb nécessite un pop + push + vérification ; en Rust, c'est une seule ligne.

C sort_two.c · cost_sort (excerpt) 313 lines total
/* For each element in B, compute cost to insert into A */
while (are_left(pile_b))
{
    i = 0;
    while (i < pile_len(pile_b))
    {
        tmp = pass_pile(pile_b, i);
        pos_a = find_insert_pos(pile_a, tmp);
        cost = compute_cost(i, pos_a,
                            pile_len(pile_a),
                            pile_len(pile_b));
        if (cost < best) {
            best = cost;
            best_idx = i;
            best_pos = pos_a;
        }
        i++;
    }
    execute_strategy(pile_a, pile_b,
                       best_idx, best_pos);
    pab(pile_a, pile_b, "pa", 1);
}
Rust sort.rs · cost_sort (excerpt) 179 lines total
// For each element in B, compute cost to insert into A
while !b.is_empty() {
    let (mut best, mut best_idx, mut best_pos)
        = (i32::MAX, 0, 0);

    for i in 0..b.len() {
        let tmp = b[i];
        let pos_a = find_insert_pos(&a, tmp);
        let cost = compute_cost(
            i, pos_a, a.len(), b.len(),
        );
        if cost < best {
            best     = cost;
            best_idx = i;
            best_pos = pos_a;
        }
    }

    execute_strategy(
        &mut a, &mut b, best_idx, best_pos,
        &mut ops, verbose,
    );
    pa(&mut a, &mut b, &mut ops, verbose);
}
🔁

Same logic, different ergonomics Même logique, ergonomie différente

Both versions implement find_insert_pos, compute_cost, execute_strategy, and sort_three identically. The Rust version is shorter because VecDeque indexing, for i in 0..n, and Option<T> replace manual linked-list traversal, pass_pile, and null checks. Les deux versions implémentent find_insert_pos, compute_cost, execute_strategy et sort_three à l'identique. La version Rust est plus courte car l'indexation VecDeque, for i in 0..n et Option<T> remplacent le parcours manuel de liste chaînée, pass_pile et les vérifications de nullité.

5 C files deleted, 1 libft deleted 5 fichiers C supprimés, 1 libft supprimée

The biggest savings come from deletion, not refactoring. Five C files — 414 lines — simply have no Rust counterpart, because the standard library already provides their functionality. Add the ~2000-line libft, and Rust deletes ~2414 lines of infrastructure code that does nothing push_swap-specific. Les plus grosses économies viennent de la suppression, pas du remaniement. Cinq fichiers C — 414 lignes — n'ont simplement pas de pendant Rust, car la bibliothèque standard fournit déjà leur fonctionnalité. Ajoutez la libft de ~2000 lignes, et Rust supprime ~2414 lignes de code d'infrastructure qui ne fait rien de spécifique à push_swap.

🗑️ Files eliminated (414 C lines deleted) 🗑️ Fichiers supprimés (414 lignes C supprimées)

treatment.c new_node · push · pop · free_pile
Hand-rolled linked-list node allocator and four operations on it. Rust's VecDeque::push_front / pop_front / push_back / pop_back do all four natively in O(1). No malloc(sizeof(t_pile)), no manual free. Allocateur de nœud de liste chaînée écrit à la main avec quatre opérations. VecDeque::push_front / pop_front / push_back / pop_back de Rust fait les quatre nativement en O(1). Pas de malloc(sizeof(t_pile)), pas de free manuel.
−74
lines
treatment2.c end_node · are_left · pass_pile
Helpers to walk the linked list: find the tail, check if non-empty, get the i-th element. Rust replaces them with VecDeque::len() · is_empty() · b[i] — direct indexing, no traversal. Aides pour parcourir la liste chaînée : trouver la queue, vérifier la non-vacuité, obtenir le i-ème élément. Rust les remplace par VecDeque::len() · is_empty() · b[i] — indexation directe, sans parcours.
−56
lines
verif.c find_median · median_between
Median-finding for the quicksort approach. But the cost-based insertion sort that sort.rs uses doesn't need medians at all — it computes per-element insertion cost directly. The whole file is dead code under the chosen algorithm. Recherche de médiane pour l'approche quicksort. Mais le tri par insertion à coût utilisé par sort.rs n'a pas besoin de médianes — il calcule le coût d'insertion par élément directement. Le fichier entier est du code mort sous l'algorithme choisi.
−114
lines
verif2.c pile_len · pile_min · pile_max · closest_num · islarger
Five utility functions: length, min, max, closest number, and overflow check. Rust: VecDeque::len() · iter().min() · iter().max() · i32::parse(). The overflow check is built into parse — it returns Err on values outside i32 range. Cinq fonctions utilitaires : longueur, min, max, nombre le plus proche, vérification de débordement. Rust : VecDeque::len() · iter().min() · iter().max() · i32::parse(). La vérification de débordement est intégrée à parse — il retourne Err pour les valeurs hors plage i32.
−102
lines
valid_input.c is_num · is_dup · duplicates · islarger
Input validation: is the string a number? Are there duplicates? Is the number in int range? Rust's parse_args (22 lines) does all three with i32::parse + HashSet::insert. Validation d'entrée : la chaîne est-elle un nombre ? Y a-t-il des doublons ? Le nombre est-il dans la plage int ? parse_args de Rust (22 lignes) fait les trois avec i32::parse + HashSet::insert.
−68
lines

📦 The libft elimination (~2000 lines deleted) 📦 L'élimination de la libft (~2000 lignes supprimées)

The C version links a personal libft — about 30 reimplemented libc functions, ~2000 lines. None of it is push_swap-specific; it's pure infrastructure. Rust's standard library replaces every single function: La version C lie une libft personnelle — environ 30 fonctions libc réécrites, ~2000 lignes. Rien de spécifique à push_swap ; c'est de la pure infrastructure. La bibliothèque standard de Rust remplace chaque fonction :

libft functionFonction libft What it doesCe qu'elle fait Rust replacement
ft_atoiparse string → intstr::parse::<i32>()
ft_strcmpcompare two strings== on &str
ft_strsplitsplit string by delimiterstr::split_whitespace()
ft_putstrwrite string to stdoutprint!()
ft_putendlwrite string + newlineprintln!()
ft_isdigitcheck ASCII digitchar::is_ascii_digit()
get_next_lineread line from fdstdin.lock().lines()
ft_printfformatted outputformat!() / println!()
ft_strjoinconcatenate two stringsString + &str / format!()
ft_strdupcopy a stringString::from() / .to_string()
ft_strnewallocate zeroed stringString::with_capacity() / new()
ft_strlenstring lengthstr::len()
ft_memallocallocate zeroed memoryvec![0; n] / Box::new
ft_strdelfree a string(automatic, Drop)
ft_putcharwrite single charprint!("{}", c)
ft_strsubsubstring&s[a..b]
ft_itoaint → stringi32::to_string()
📦

Why libft exists — and why Rust doesn't need it Pourquoi la libft existe — et pourquoi Rust n'en a pas besoin

The 42 curriculum forbids using the standard C library beyond a tiny allowlist (read, write, malloc, free, exit). So students build libft project after project — the same ft_atoi, the same get_next_line, the same ft_printf. Rust has no such restriction: cargo pulls crates, and std already covers everything libft does, plus Vec, HashMap, VecDeque, threads, and async. Le cursus 42 interdit d'utiliser la bibliothèque C standard au-delà d'une petite liste autorisée (read, write, malloc, free, exit). Les étudiants construisent donc libft projet après projet — le même ft_atoi, le même get_next_line, le même ft_printf. Rust n'a pas une telle restriction : cargo tire des crates, et std couvre déjà tout ce que libft fait, plus Vec, HashMap, VecDeque, les threads et l'async.

Same algorithm, same operation count Même algorithme, même nombre d'opérations

The cost-based insertion sort is deterministic: given the same input, both implementations make the same choices. Rust is +6% on 100 numbers (VecDeque bookkeeping vs raw pointers) and −1% on 500 (better cache locality). Both pass the 42 grading thresholds — 700 ops for 100 numbers, 5500 for 500 — with full marks. Le tri par insertion à coût est déterministe : pour la même entrée, les deux implémentations font les mêmes choix. Rust est +6% sur 100 nombres (bookkeeping VecDeque vs pointeurs bruts) et −1% sur 500 (meilleure localité de cache). Les deux passent les seuils de notation 42 — 700 ops pour 100 nombres, 5500 pour 500 — avec la note maximale.

100 random numbers · average over 1000 runs · threshold < 700 ops for 5/5 100 nombres aléatoires · moyenne sur 1000 runs · seuil < 700 ops pour 5/5
Rust
594 ops
+6%
C
559 ops
baseline
limit
700 ops (5/5 threshold)
500 random numbers · average over 1000 runs · threshold < 5500 ops for 5/5 500 nombres aléatoires · moyenne sur 1000 runs · seuil < 5500 ops pour 5/5
Rust
5231 ops
−1%
C
5281 ops
baseline
limit
5500 ops (5/5 threshold)
TestTest Rust (avg ops) C (avg ops) DifferenceDifférence VerdictVerdict
100 numbers 594 559 +6% ✓ 5/5 (< 700)
500 numbers 5231 5281 −1% ✓ 5/5 (< 5500)
3 numbers (worst case) ≤ 2 ops ≤ 2 ops 0% ✓ identical
5 numbers (worst case) ≤ 10 ops ≤ 10 ops 0% ✓ identical
Already sorted (any N) 0 ops 0 ops 0% ✓ early exit
2 elements, descending 1 op (sa) 1 op (sa) 0% ✓ identical
🎯

Why the +6% on 100 numbers? Pourquoi le +6% sur 100 nombres ?

VecDeque stores elements in a growable ring buffer; raw C pointers walk a linked list one node at a time. On small inputs (N=100), the VecDeque's bounds checks and capacity management cost slightly more than pointer chasing. At N=500, cache locality flips the table: contiguous memory beats pointer dereferences, and Rust edges ahead. VecDeque stocke les éléments dans un tampon circulaire extensible ; les pointeurs C bruts parcourent une liste chaînée un nœud à la fois. Sur les petites entrées (N=100), les vérifications de bornes et la gestion de capacité du VecDeque coûtent légèrement plus que le chaînage de pointeurs. À N=500, la localité de cache renverse la table : la mémoire contiguë bat les dereferences de pointeurs, et Rust prend l'avantage.

malloc/free vs the Drop trait malloc/free vs le trait Drop

In C, every node is a malloc, every removal is a free, and forgetting free_pile at program exit is a leak that the 42 moulinette will punish with grade 0. In Rust, VecDeque owns its elements and the Drop trait releases them automatically when they go out of scope. The compiler refuses to compile a use-after-free. En C, chaque nœud est un malloc, chaque suppression un free, et oublier free_pile à la sortie du programme est une fuite que la moulinette 42 punira d'une note 0. En Rust, VecDeque possède ses éléments et le trait Drop les libère automatiquement quand ils sortent du scope. Le compilateur refuse de compiler un use-after-free.

🧱
AllocationAllocation
C · malloc
malloc(sizeof(t_pile)) per node
Rust · VecDeque
manages internally
🧹
DeallocationLibération
C · manual
in pop() & free_pile()
Rust · automatic
Drop trait at end of scope
💥
Leak riskRisque de fuite
C · high
forget free_pile → grade 0
Rust · zero
compiler enforces ownership
🔗
Dangling ptrPtr dangling
C · possible
use-after-free
Rust · impossible
borrow checker rejects
🔍
VerificationVérification
C · Valgrind
required at every grade
Rust · not needed
proven at compile time

The C pop function — and what Rust does instead La fonction C pop — et ce que Rust fait à la place

This is the C version's pop: free the head node, advance the pointer. If you forget the free, you leak. If you free twice, you crash. Rust's VecDeque::pop_front returns Option<i32> and the memory is reclaimed when the VecDeque is dropped. Voici le pop de la version C : libérer le nœud de tête, avancer le pointeur. Si vous oubliez le free, vous fuyez. Si vous free deux fois, vous crash. Le VecDeque::pop_front de Rust retourne Option<i32> et la mémoire est récupérée quand le VecDeque est droppé.

C treatment.c · pop() manual free
int pop(t_pile **p)
{
    t_pile *tmp;
    int     x;

    if (!*p)
        return (0);          /* silent failure */
    tmp = *p;
    x   = (*p)->x;
    *p   = (*p)->next;
    free(tmp);          /* forget me = leak */
    return (x);
}

void free_pile(t_pile **p)
{
    while (*p)
        pop(p);
    /* if you forget to call me → grade 0 */
}
Rust operations.rs · pa() compiler-enforced
// VecDeque::pop_front returns Option<i32>.
// No free() needed — Drop handles it.
// No null check — Option forces the match.

pub fn pa(
    a: &mut VecDeque<i32>,
    b: &mut VecDeque<i32>,
    ops: &mut Vec<String>,
    verbose: bool,
) {
    if let Some(val) = b.pop_front() {
        a.push_front(val);
        print_op("pa", ops, verbose);
    }
}

// No free_pile() equivalent exists.
// When `a` and `b` go out of scope in main(),
// their Drop impls free everything.
⚠️

The 42 moulinette reality La réalité de la moulinette 42

On the C version, the 42 auto-grader runs Valgrind. If it reports a single "definitely lost" byte, the grade is 0. The Rust version cannot leak by construction — VecDeque's Drop impl runs when the binding leaves scope, period. There is no Valgrind step, because there is nothing for Valgrind to find. Sur la version C, l'auto-évaluateur 42 lance Valgrind. S'il rapporte un seul octet « définitivement perdu », la note est 0. La version Rust ne peut pas fuir par construction — l'impl Drop de VecDeque s'exécute quand la variable sort du scope, point. Il n'y a pas d'étape Valgrind, parce qu'il n'y a rien à trouver pour Valgrind.

Cost-based insertion sort, unchanged Tri par insertion à coût, inchangé

The Rust version uses the exact same algorithm as the C version. Not a similar algorithm — the same one. Both files implement cost-based insertion sort: push everything to B, then repeatedly bring back the element that costs the fewest rotations to insert in its correct A position. Here is the 4-step recap. La version Rust utilise exactement le même algorithme que la version C. Pas un algorithme similaire — le même. Les deux fichiers implémentent le tri par insertion à coût : tout pousser vers B, puis ramener répétitivement l'élément qui coûte le moins de rotations à insérer à sa position correcte dans A. Voici le récapitulatif en 4 étapes.

STEP 01

Push everything to B Tout pousser vers B

For inputs larger than 5, pb every element of A into B. After this phase, A is empty (or holds 3 elements for sort_three) and B holds everything else in arbitrary order. Pour les entrées supérieures à 5, pb chaque élément de A vers B. Après cette phase, A est vide (ou contient 3 éléments pour sort_three) et B contient tout le reste dans un ordre arbitraire.

impl: while a.len() > 3 { pb(...) } impl: while a.len() > 3 { pb(...) }
STEP 02

Find the cheapest element in B Trouver l'élément le moins cher de B

For each element in B, compute its insertion cost: how many rotations of A and B are needed to bring it to A's correct position. Pick the element with the lowest cost — considering all four rotation strategies (ra+rb, rra+rrb, ra+rrb, rra+rb). Pour chaque élément de B, calculer son coût d'insertion : combien de rotations de A et B sont nécessaires pour l'amener à la bonne position dans A. Choisir l'élément au coût le plus bas — en considérant les quatre stratégies de rotation (ra+rb, rra+rrb, ra+rrb, rra+rb).

impl: find_insert_pos + compute_cost impl: find_insert_pos + compute_cost
STEP 03

Execute the optimal rotation strategy Exécuter la stratégie de rotation optimale

Of the 4 strategies, execute the one whose cost was lowest. Use combined rotations (rr = ra+rb, rrr = rra+rrb) when both stacks rotate in the same direction — saving one op per shared rotation. Parmi les 4 stratégies, exécuter celle dont le coût était le plus bas. Utiliser les rotations combinées (rr = ra+rb, rrr = rra+rrb) quand les deux piles tournent dans le même sens — économisant une op par rotation partagée.

impl: execute_strategy picks min of 4 strategies impl: execute_strategy choisit le min des 4 stratégies
STEP 04

Push back & repeat Repousser & répéter

pa the chosen element to A. Repeat from step 2 until B is empty. Finally, rotate A so its smallest element is at the front. For inputs of size ≤ 5, skip steps 1–4 and use a hardcoded mini_solve / sort_three. pa l'élément choisi vers A. Répéter depuis l'étape 2 jusqu'à ce que B soit vide. Enfin, faire tourner A pour que son plus petit élément soit en tête. Pour les entrées de taille ≤ 5, sauter les étapes 1–4 et utiliser un mini_solve / sort_three codé en dur.

impl: while !b.is_empty() + final rotate_to_min impl: while !b.is_empty() + rotate_to_min final
📖

Want the full algorithm breakdown? Vous voulez l'analyse complète de l'algorithme ?

This page focuses on the Rust rewrite story: what changed, what got deleted, what got safer. For the deep-dive on the cost-based insertion sort itself — with diagrams of the 4 rotation strategies, worked examples, and the math behind compute_cost — see the original C-version guide. Cette page se concentre sur l'histoire de la réécriture Rust : ce qui a changé, ce qui a été supprimé, ce qui est devenu plus sûr. Pour l'analyse approfondie du tri par insertion à coût — avec diagrammes des 4 stratégies de rotation, exemples détaillés, et les maths derrière compute_cost — voir le guide original de la version C.

cargo build --release cargo build --release

No Makefile, no libft link step, no -Wall -Wextra -Werror. Cargo handles compilation, dependency resolution, and the binary layout. Both push_swap and checker are built from the same crate via Cargo's [[bin]] targets. Pas de Makefile, pas d'étape de link libft, pas de -Wall -Wextra -Werror. Cargo gère la compilation, la résolution des dépendances et la disposition des binaires. push_swap et checker sont construits depuis la même crate via les targets [[bin]] de Cargo.

① Build the binaries ① Compiler les binaires

z@machine:~/push-swap-rust
# Build both binaries in release mode (optimized)
$ cargo build --release
   Compiling push_swap_rust v1.0.0 (/home/z/push-swap-rust)
    Finished release [optimized] target(s) in 0.42s

# Binaries are now in target/release/
$ ls -la target/release/
-rwxr-xr-x  push_swap
-rwxr-xr-x  checker

# (Debug build for development)
$ cargo build

② Run push_swap on 100 random numbers ② Lancer push_swap sur 100 nombres aléatoires

z@machine:~/push-swap-rust
# Generate 100 unique numbers, run push_swap, count ops
$ ARG=$(shuf -i 1-1000 -n 100 | tr '\n' ' ')
$ ./target/release/push_swap "$ARG" | wc -l
594

# Verify the result with the checker
$ ./target/release/push_swap "$ARG" | ./target/release/checker "$ARG"
OK

# Verbose mode (prints each op to stderr as it executes)
$ ./target/release/push_swap -v "3 1 2"
sa
ra

③ Run the test suite ③ Lancer la suite de tests

z@machine:~/push-swap-rust
# Unit tests for parser, operations, sort
$ cargo test
   Compiling push_swap_rust v1.0.0
    Finished test [unoptimized + debuginfo] target(s)
     Running unittests
...
test result: ok. 24 passed; 0 failed; 0 ignored

# Lint with clippy (Rust's strict linter)
$ cargo clippy -- -D warnings
    Finished
No warnings. Clean.

# Format check
$ cargo fmt --check
(no output = formatted correctly)

④ Benchmark against the 42 grading thresholds ④ Tester contre les seuils de notation 42

z@machine:~/push-swap-rust
# Run 1000 tests with 100 numbers, report average
$ total=0; for i in $(seq 1 1000); do
    ARG=$(shuf -i 1-1000 -n 100 | tr '\n' ' ')
    ops=$(./target/release/push_swap "$ARG" | wc -l)
    total=$((total + ops))
done; echo "avg: $((total / 1000)) ops"
avg: 594 ops  # well under 700 → 5/5

# Same for 500 numbers
$ total=0; for i in $(seq 1 1000); do
    ARG=$(shuf -i 1-5000 -n 500 | tr '\n' ' ')
    ops=$(./target/release/push_swap "$ARG" | wc -l)
    total=$((total + ops))
done; echo "avg: $((total / 1000)) ops"
avg: 5231 ops # well under 5500 → 5/5
🦀

Project layout Disposition du projet

push-swap-rust/
├── Cargo.toml # crate manifest + 2 [[bin]] targets
├── Cargo.lock
└── src/
    ├── lib.rs # 5 lines — pub mod declarations
    ├── main.rs # 65 lines — push_swap binary
    ├── checker.rs # 81 lines — checker binary
    ├── parser.rs # 22 lines — input validation
    ├── operations.rs # 99 lines — sa/sb/pa/pb/ra/rra/...
    ├── sort.rs # 179 lines — cost_sort + mini_solve
    └── utils.rs # 56 lines — is_sorted, find_min, ...
                         total: 507 lines