malloc(sizeof(t_pile)) per nodeWhy 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.
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().
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.
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.
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.
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").
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
🦀 Rust version (7 files) 507
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.
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); }
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)).
/* 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)); }
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.
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);
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.
/* 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); }
// 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)
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.
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.
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.
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.
📦 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_atoi | parse string → int | str::parse::<i32>() |
| ft_strcmp | compare two strings | == on &str |
| ft_strsplit | split string by delimiter | str::split_whitespace() |
| ft_putstr | write string to stdout | print!() |
| ft_putendl | write string + newline | println!() |
| ft_isdigit | check ASCII digit | char::is_ascii_digit() |
| get_next_line | read line from fd | stdin.lock().lines() |
| ft_printf | formatted output | format!() / println!() |
| ft_strjoin | concatenate two strings | String + &str / format!() |
| ft_strdup | copy a string | String::from() / .to_string() |
| ft_strnew | allocate zeroed string | String::with_capacity() / new() |
| ft_strlen | string length | str::len() |
| ft_memalloc | allocate zeroed memory | vec![0; n] / Box::new |
| ft_strdel | free a string | (automatic, Drop) |
| ft_putchar | write single char | print!("{}", c) |
| ft_strsub | substring | &s[a..b] |
| ft_itoa | int → string | i32::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.
| 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.
pop() & free_pile()free_pile → grade 0
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é.
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 */ }
// 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.
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.
while a.len() > 3 { pb(...) }
impl: while a.len() > 3 { pb(...) }
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).
find_insert_pos + compute_cost
impl: find_insert_pos + compute_cost
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.
execute_strategy picks min of 4 strategies
impl: execute_strategy choisit le min des 4 stratégies
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.
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
# 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
# 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
# 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
# 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