SmartKitio

Programmation

Que se passe-t-il vraiment quand le code s’exécute ?

Le programme à l’écran est du texte. Celui qui tourne, c’est de la mémoire, des registres et des appels système qui changent dans le temps. Entre les deux se trouve un pipeline — lexage, analyse, vérifications, génération de code, optimisation et services runtime — qui explique erreurs de syntaxe, traces de pile et l’écart entre « ça marche chez moi » et la production.

9 min de lecture · Mis à jour le 2026-10-04

Espace de travail de développeur avec du code à l’écran et un éclairage chaud
Photo via Unsplash · Licence Unsplash

Le code source n’est pas encore un programme

Les fichiers source sont des caractères encodés en UTF-8 (ou un autre encodage attendu par votre toolchain). Le système d’exploitation n’exécute pas le texte directement — il charge un format exécutable (ELF, Mach-O, PE) ou démarre un runtime qui interprète du bytecode. Le premier travail d’une implémentation de langage est de transformer le texte en représentation structurée sur laquelle elle peut raisonner.

Du texte à la structure

Pipeline classique de compilation / exécution
  1. Source

    Fichiers .rs, .java, .js, .py écrits par des humains

  2. Lexer

    Scanner les caractères en tokens (mots-clés, identifiants, littéraux)

  3. Parser

    Construire un AST selon les règles de grammaire

  4. Analyse sémantique

    Types, portées, variables non définies — erreurs avant exécution

  5. IR / bytecode / code machine

    Abaisser l’AST en ops portables ou instructions natives

  6. Optimiseur

    Inlining, élimination de code mort, réordonnancement CPU (passe optionnelle)

  7. Runtime / CPU

    Exécuter sur le matériel ou une VM avec services OS

Les étapes exactes varient selon le langage ; le sens du flux de données est le même.

Les erreurs de syntaxe échouent au lexer ou au parser (« unexpected token »). Les erreurs de type ou de nom échouent souvent en analyse sémantique. Les erreurs de logique passent tous les contrôles et n’apparaissent que quand des entrées précises atteignent des branches bugguées — d’où l’importance des tests et de l’observabilité après un pipeline réussi.

Interprété vs compilé

L’interprétation pure parcourt l’AST ou le bytecode et agit immédiatement — simple à bootstrapper, plus lent par étape. La compilation ahead-of-time (AOT) traduit des programmes entiers en code machine avant exécution — les builds release C, Rust et Go se comportent ainsi. Beaucoup de langages populaires mélangent les deux : Python compile en bytecode puis interprète ; Java livre du bytecode et JIT-compile les méthodes chaudes ; JavaScript analyse une fois, exécute des chemins rapides via des niveaux de JIT, et retombe sur une interprétation lente quand les types changent.

Stratégies d’exécution (simplifié)

StratégieIdéeExemples
Interprétation par arbreVisiter les nœuds AST et agirInterpréteurs pédagogiques anciens
Bytecode + VMCompiler en instructions portables ; la VM dispatch les opcodesBytecode CPython, JVM Java, IL C#
Compilation native AOTCode machine avant exécutionRust, C release, Go par défaut
JITCompiler les chemins chauds à l’exécutionMoteurs JS (V8, SpiderMonkey), JVM HotSpot
Transpiler puis exécuterSource vers source, puis runtime existantTypeScript → JavaScript

Interpréteur vs AOT vs JIT (quand chacun gagne)

ApprocheQuand le travail se faitDémarrageVitesse de pointeCompromis typique
Interpréteur / VM bytecodeChaque instruction à l’exécutionRapidePlus faible jusqu’au warmupPortable, builds prévisibles ; boucles chaudes coûtent plus
Compilation AOTAvant le démarrage du processusDépend de la taille du binaireÉlevée pour du code natifBuilds plus longs ; binaire par CPU/OS ; erreurs tôt
JITPremiers runs interprétés ; code chaud compilé ensuiteAssez rapide pour naviguerTrès élevée sur chemins stablesOverhead de profilage ; deopt si hypothèses cassées

Choisir entre eux est rarement idéologique. Jeux mobile et outils CLI favorisent des binaires AOT sans surprise de VM. Serveurs Node ou JVM longue durée amortissent le warmup JIT. Scripts courts et sessions REPL profitent de l’interprétation sans attendre l’optimisation. Votre rôle est de savoir quel pipeline utilise votre runtime pour interpréter profils et traces — pas de mémoriser chaque marque de moteur.

Bytecode et machines virtuelles

Le bytecode est un jeu d’instructions compact pour une machine virtuelle — pas lié à une architecture CPU. Les fichiers .class Java, .pyc Python et modules WebAssembly ciblent des VM qui imposent sécurité mémoire ou règles de sandbox. La VM gère conventions d’appel, exceptions, et parfois compilation JIT en code natif pour les boucles chaudes.

La VM se place entre votre source et l’OS : elle peut vérifier le bytecode avant exécution (Java), imposer des règles de sandbox (WASM navigateur), ou insérer des hooks pour débogueurs et profileurs. Cette indirection coûte du CPU mais achète la portabilité — le même .jar tourne sur ARM et x86 — et des modèles d’erreur uniformes comme les exceptions Java au lieu de segfaults sur de mauvais pointeurs. Quand vous attachez un débogueur à Node ou Python, vous arrêtez souvent la VM entre instructions bytecode, puis remontez aux lignes source via métadonnées de debug générées à la compilation.

Compilation JIT en JavaScript moderne

Les moteurs navigateur et Node.js analysent JavaScript, construisent des structures internes, et exécutent vite via des interpréteurs. Le profilage trouve les fonctions chaudes ; les compilateurs JIT (souvent multi-niveaux) émettent du code machine optimisé spécialisé pour les types observés. La déoptimisation arrive quand les hypothèses cassent — d’où des sites d’appel monomorphes plus rapides que des types qui changent sans cesse dans des boucles serrées.

Les JIT multi-niveaux démarrent en tier baseline bon marché, puis promeuvent les fonctions fréquentes vers des compilateurs optimisants qui spécialisent pour entiers, doubles ou formes d’objets réellement vues aux sites d’appel. Quand un type de propriété passe de number à string, le moteur peut sortir vers un chemin lent et re-profiler. C’est pourquoi les micro-benchmarks mentent : une fonction rapide isolée peut se déoptimiser dans une app plus large où les types varient. Les modules WASM évitent une partie de cette danse en arrivant précompilés avec types explicites, au prix d’une toolchain différente.

Mémoire : pile vs tas

Voyez la pile comme du brouillon pour la chaîne d’appels courante, et le tas comme un tableau d’affichage où vivent les objets jusqu’à ce que plus rien ne pointe vers eux. Quand main appelle fetchUser, qui appelle parseJson, chaque appel pousse une frame : des locaux comme id et des tampons temporaires restent dans cette frame et disparaissent au retour. Quand parseJson renvoie un nouvel objet, sa mémoire vient du tas car l’appelant en a encore besoin après que la frame de parseJson est dépilée.

  • La pile d’appels tient des enregistrements d’activation : variables locales, adresses de retour et frames pour chaque appel de fonction.
  • Stack overflow signifie récursion trop profonde ou appels mutuels incontrôlés — pas « trop de variables » en abstract.
  • Le tas tient des objets à durée de vie dynamique : tableaux, closures, tout ce qui survit à une frame.
  • Les langages diffèrent : Rust trace la propriété à la compilation ; C attend un free manuel ; Java et JS utilisent des garbage collectors.
javascript
function total(items) {
  let sum = 0;           // stack: primitive in this frame
  for (const x of items) {
    sum += x.price;      // stack: loop locals
  }
  return { sum, count: items.length }; // heap: new object outlives this frame
}
const cart = [{ price: 9 }, { price: 4 }];
const result = total(cart); // stack: result reference; heap: { sum, count }

Garbage collection

Les GC traçants trouvent les objets atteignables depuis les racines (globaux, piles, registres) et récupèrent le reste. Les pauses dépendent de la taille du tas, de l’algorithme du collecteur, et si le runtime utilise une collection générationnelle ou concurrente. Les fuites mémoire existent encore en langages GC quand vous retenez des références vers des objets dont vous n’avez plus besoin — le GC libère l’inaccessible, pas l’oublié.

Système d’exploitation et CPU

Les instructions machine chargent des valeurs dans les registres, branchent, appellent des bibliothèques partagées, et émettent des syscalls pour I/O fichier, sockets réseau et mappage mémoire. Le noyau planifie les threads, applique les permissions, et gère les page faults quand la RAM physique se remplit. Votre runtime de langage est au-dessus — mais le profilage montre encore contention syscall et verrous quand le code « semble lent ».

Déboguer à travers le pipeline

  • La coloration syntaxique s’arrête où commence le parser — les erreurs sont statiques.
  • Points d’arrêt et pas-à-pas mappent vers bytecode ou code machine avec infos de debug.
  • console.log et logs structurés observent l’état runtime sans arrêter le temps.
  • Reproduire des bugs prod exige souvent les mêmes flags de build, variables d’environnement et formes de données — pas seulement le même fichier source.

Les outils d’observabilité sont à la frontière runtime : les profileurs échantillonnent les piles (encore des frames), les traceurs suivent les syscalls, les snapshots tas énumèrent les objets tas vivants. Quand la prod ne livre que des bundles minifiés, activez les source maps dans votre tracker d’erreurs pour des piles symboliques ; sinon vous déboguez des noms générés comme n et t au lieu de fetchUser et parseJson. Le dev local sans minification masque cet écart jusqu’au premier déploiement.

Réalités de production

Livrer du code ajoute bundlers, minifiers, tree-shaking et configuration par environnement. Les runtimes serveur ajoutent gestionnaires de processus, conteneurs et scaling horizontal — chaque instance exécute le même pipeline indépendamment. Le code client ajoute téléchargement, analyse, compilation et budgets d’exécution sur le thread principal, d’où le lien avec nos articles navigateur et client vs serveur.

Source maps et minification

Les minifiers renomment les variables en une lettre et suppriment les espaces pour des bundles plus légers ; le CPU exécute encore du JavaScript valide, mais les numéros de ligne dans les erreurs ne correspondent plus à votre éditeur. Les source maps sont des fichiers sidecar (ou commentaires inline) qui mappent positions bundle vers TypeScript ou JSX d’origine. DevTools et certains agrégateurs de logs les consomment quand vous déployez maps avec les assets — ou les uploadez vers des trackers d’erreurs. Sans maps, une panne à bundle.js:1:48291 est un état d’exécution réel à décoder ; avec maps, vous voyez la même trace qu’en dev local. Ne publiez jamais de source maps sur des URL publiques si elles embarquent le source complet sauf si vous l’acceptez ; beaucoup d’équipes limitent les maps à des serveurs de symboles internes.

À retenir

  • Le texte source passe par lex, parse et analyse avant tout travail CPU ; erreurs statiques et erreurs runtime surviennent à des étapes différentes.
  • Interpréteurs, VM bytecode, AOT et JIT sont des points sur un spectre — la plupart des langages que vous utilisez en combinent plusieurs.
  • Les frames de pile sont de l’espace brouillon par appel ; les objets tas survivent entre appels jusqu’à réclamation ou libération par le runtime.
  • Les transformations prod (minify, bundle, optimize) changent noms de fichiers et numéros de ligne sauf si les source maps comblent l’écart.
  • Profilage et logs observent le runtime qui a vraiment tourné, pas le fichier que vous auriez voulu exécuter.

Essayez le json-formatter et le regex-tester de SmartKitio dans le navigateur : ils s’exécutent dans la même VM JavaScript et le même pipeline JIT décrit ici, donc les charges collées restent sur votre machine pendant que vous explorez analyse, correspondances et comportement runtime sans déployer de code.

Approfondissements

  1. Crafting Interpreters (book)
  2. MDN — JavaScript engine fundamentals
  3. Aho, Lam, Sethi, Ullman — Compilers: Principles, Techniques, and Tools

Articles associés