J’ai lu certaines réponses en diagonale, désolé pour les redites éventuelles, mais voici une réponse à la question initiale :
Tous les langages à typage dynamique sont interprétés (Python, PHP, Javascript, ..), et tous les langages compilés sont à typage statique (C, C++, D, Java, Go, ..). Avez-vous un contre-exemple ?
Common Lisp.
Il possède un typage dynamique (tu peux difficilement faire plus dynamique, même), mais la majorité des implémentations sérieuses sont des compilateurs : LispWorks, Allegro, SBCL, CMU CL… D’ailleurs SBCL (et sûrement les autres) te permet même d’intégrer de l’assembleur en inline au milieu de tes fonctions.
Attention : les compilateurs que j’ai cités peuvent ressembler à des interpréteurs ou à du JIT (tu as une REPL), mais ce sont bien de «véritables» compilateurs. Ils prennent du code Lisp en entrée et génèrent du code assembleur x86 en sortie. Ils sont parfois appelés à la volée pour «émuler» de l’interprétation, mais dans la majorité des cas le code assembleur est utilisé pour générer un exécutable.
Voici comment fonctionne SBCL (c’est l’exemple que je connais le mieux, mais les autres font sans doute pareil). Chaque fois que le compilateur rencontre une primitive du langage (par exemple une addition), il génère du code assembleur qui teste si les opérandes sont du bon type (dans l’exemple, des entiers) et qui renvoie le bon résultat si c’est le cas. Autrement, une exception est levée. Les exceptions sont compilées de façon classique, un peu comme en C++ ou en Java (il y a une subtilité pour gérer la reprise sur panne, mais tout reste géré en assembleur de façon statique).
Si on applique la technique ci-avant naïvement, le code généré sera atrocement lent. Il faut donc optimiser avec de la propagation de types. Par exemple, si on rencontre une addition suivie d’une multiplication, avec les mêmes opérandes, il n’y a pas besoin de tester deux fois que ces opérandes sont bien des entiers : on peut éliminer le test de types la deuxième fois. En utilisant des algos assez évolués, de la même famille que l’inférence de types à la Caml/Haskell, on peut éliminer la majorité des tests de types et ainsi obtenir un code rapide.
Bien sûr, il y aura toujours des situations où le compilateur ne «sait pas», et doit donc garder les tests de type au cas où. Dans ces cas-là, SBCL affiche un warning en expliquant de son mieux pourquoi il n’a pas pu optimiser le test de type. Assez souvent, on peut ajouter des annotations ou changer légèrement le code pour qu’il puisse le faire quand même.
Bien sûr, si tu es pervers, tu peux imaginer des exemples particulièrement défavorables où il est très difficile d’éliminer les tests de types, même à la main. Mais ces exemples n’apparaissent que très rarement dans du code lambda (pun intended).