Outre Solidity, quels autres langages EVM méritent qu’on s’y intéresse ?

Écrit par : jtriley.ethjtriley.eth

Compilé par : 0x11, Foresight News

La machine virtuelle Ethereum (EVM) est une machine de Turing 256 bits, basée sur une pile et accessible dans le monde entier. Étant donné que l'architecture est très différente des autres machines virtuelles et physiques, EVM nécessite un langage DSL spécifique au domaine (remarque : un langage spécifique au domaine fait référence à un langage informatique qui se concentre sur un certain domaine d'application).

Dans cet article, nous examinerons l'état de l'art en matière de conception EVM DSL, couvrant six langages : Solidity, Vyper, Fe, Huff, Yul et ETK.

Version linguistique

  • Solidité : 0.8.19

  • Vyper : 0.3.7

  • Fe: 0,21,0

  • Souffler: 0.3.1

  • ETK : 0.2.1

  • Yul: 0.8.19

La lecture de cet article nécessite que vous ayez une compréhension de base de l'EVM, de la pile et de la programmation.

Présentation de la machine virtuelle Ethereum

L'EVM est une machine de Turing basée sur une pile de 256 bits. Cependant, avant de se plonger dans son compilateur, certaines fonctionnalités fonctionnelles doivent être introduites.

Parce que l'EVM est « Turing complet », il souffrira du « problème d'arrêt ». En bref, avant qu’un programme ne s’exécute, il n’existe aucun moyen de déterminer s’il se terminera dans le futur. La manière dont l'EVM résout ce problème consiste à mesurer les unités de calcul via le « Gaz », qui est généralement proportionnel aux ressources physiques nécessaires pour exécuter les instructions. La quantité de Gaz par transaction est limitée et l'initiateur de la transaction doit payer des ETH proportionnellement au Gaz consommé par la transaction. L’un des impacts de cette stratégie est que s’il existe deux contrats intelligents fonctionnellement identiques, celui qui consomme le moins de gaz sera davantage adopté. Il en résulte des protocoles en concurrence pour une efficacité énergétique extrême, les ingénieurs s'efforçant de minimiser la consommation de gaz pour des tâches spécifiques.

De plus, lorsqu’un contrat est appelé, il crée un contexte d’exécution. Dans ce contexte, le contrat dispose d'une pile pour les opérations et le traitement, d'une instance de mémoire linéaire pour la lecture et l'écriture, d'un stockage persistant local pour la lecture et l'écriture du contrat, et les données attachées à l'appel « calldata » peuvent être lues mais non écrites. .

Une remarque importante concernant la mémoire est que même s'il n'y a pas de « limite supérieure » définie à sa taille, elle reste limitée. Le coût en gaz de l’extension de mémoire est dynamique : une fois qu’un seuil est atteint, le coût d’extension de mémoire augmente quadratiquement, ce qui signifie que le coût en gaz est proportionnel au carré de l’allocation de mémoire supplémentaire.

Les contrats peuvent également appeler d'autres contrats en utilisant un certain nombre d'instructions différentes. L'instruction « call » envoie des données et de l'ETH facultatif au contrat cible, puis crée son propre contexte d'exécution jusqu'à ce que l'exécution du contrat cible s'arrête. La directive "staticcall" est identique à "call", mais ajoute une vérification qui affirme qu'aucune partie de l'état global n'a été mise à jour avant la fin de l'appel statique. Enfin, la directive "delegatecall" se comporte comme "call" sauf qu'elle conserve certaines informations environnementales du contexte précédent. Ceci est généralement utilisé pour les bibliothèques externes et les contrats proxy.

Pourquoi la conception linguistique est importante

Les langages spécifiques au domaine (DSL) sont nécessaires pour interagir avec des architectures atypiques. Bien qu'il existe des chaînes d'outils de compilateur telles que LLVM, s'appuyer sur elles pour gérer les contrats intelligents est loin d'être idéal dans les situations où l'exactitude du programme et l'efficacité des calculs sont essentielles.

L'exactitude du programme est importante car les contrats intelligents sont immuables par défaut et constituent un choix populaire pour les applications financières compte tenu des propriétés des machines virtuelles (VM) blockchain. Bien qu'il existe une solution évolutive pour l'EVM, il s'agit au mieux d'un correctif et au pire d'une vulnérabilité d'exécution de code arbitraire.

L’efficacité informatique est également essentielle, car minimiser les calculs présente des avantages économiques, mais pas au détriment de la sécurité.

En bref, un DSL EVM doit équilibrer l'exactitude du programme et l'efficacité du gaz, en atteignant l'un ou l'autre en faisant différents compromis sans sacrifier trop de flexibilité.

Aperçu de la langue

Pour chaque langage, nous décrirons leurs principales caractéristiques et choix de conception, et inclurons un simple contrat intelligent avec fonction de comptage. La popularité verbale est déterminée sur la base des données Total Value Locked (TVL) sur Defi Llama.

Solidité

Solidity est un langage de haut niveau dont la syntaxe est similaire à C, Java et Javascript. C'est la langue la plus populaire selon TVL, avec un TVL dix fois supérieur à celui de la deuxième langue la plus populaire. Pour la réutilisation du code, il utilise un modèle orienté objet, dans lequel les contrats intelligents sont traités comme des objets de classe, tirant parti de l'héritage multiple. Le compilateur est écrit en C++ et il est prévu de migrer vers Rust à l'avenir.

Les champs de contrat mutables sont stockés dans un stockage persistant, sauf si leurs valeurs sont connues au moment de la compilation (constantes) ou du déploiement (immuable). Les méthodes déclarées au sein d'un contrat peuvent être déclarées pures, vues, payables ou non payables par défaut mais avec un statut modifiable. Les méthodes pures ne lisent pas les données de l'environnement d'exécution et ne peuvent pas lire ou écrire sur le stockage persistant ; autrement dit, avec la même entrée, les méthodes pures renverront toujours la même sortie et n'auront aucun effet secondaire. Les méthodes View peuvent lire des données à partir du magasin de persistance ou de l'environnement d'exécution, mais elles ne peuvent pas écrire dans le magasin de persistance ni créer d'effets secondaires tels que l'ajout de journaux de transactions. Les méthodes payantes peuvent lire et écrire un stockage persistant, lire des données de l'environnement d'exécution, produire des effets secondaires et recevoir des ETH attachés à l'appel. La méthode non payable est la même que la méthode payable mais comporte une vérification d'exécution pour affirmer qu'aucun ETH n'est attaché dans le contexte d'exécution actuel.

REMARQUE : attacher l'ETH à une transaction est distinct du paiement des frais de gaz, l'ETH attaché est reçu par le contrat et vous pouvez choisir de l'accepter ou de le rejeter en restaurant le contexte.

Lorsqu'elles sont déclarées dans le cadre d'un contrat, les méthodes peuvent spécifier quatre modificateurs de visibilité : privé, interne, public ou externe. Les méthodes privées sont accessibles en interne via l'instruction "jump" dans le contrat actuel. Aucun contrat hérité ne peut accéder directement aux méthodes privées. Les méthodes internes sont également accessibles en interne via l'instruction "jump", mais les contrats hérités peuvent utiliser directement les méthodes internes. Les méthodes publiques sont accessibles par des contrats externes via l'instruction "call", qui crée un nouveau contexte d'exécution, et en interne via des sauts lors de l'appel direct de la méthode. Les méthodes publiques sont également accessibles à partir du même contrat dans un nouveau contexte d'exécution en ajoutant "this" à l'appel de méthode. Les méthodes externes ne sont accessibles que via l'instruction "call". Qu'elles proviennent de contrats différents ou au sein du même contrat, "this" doit être ajouté avant l'appel de méthode.

Remarque : L'instruction "jump" actionne le compteur du programme, et l'instruction "call" crée un nouveau contexte d'exécution lors de l'exécution du contrat cible. Lorsque cela est possible, utilisez « sauter » au lieu de « appeler » pour économiser de l'essence.

Solidity propose également trois façons de définir les bibliothèques. La première est une bibliothèque externe, qui est un contrat sans état déployé séparément sur la chaîne, lié dynamiquement lorsque le contrat est appelé et accessible via l'instruction « delegatecall ». Il s'agit de l'approche la moins courante car la prise en charge des outils pour les bibliothèques externes est insuffisante, le « delegatecall » est coûteux, il doit charger du code supplémentaire à partir du stockage persistant et nécessite le déploiement de plusieurs transactions. Les bibliothèques internes sont définies de la même manière que les bibliothèques externes, sauf que chaque méthode doit être définie comme méthode interne. Au moment de la compilation, la bibliothèque interne est intégrée au contrat final et pendant la phase d'analyse du code mort, les méthodes inutilisées de la bibliothèque sont supprimées. La troisième méthode est similaire à la bibliothèque interne, mais au lieu de définir des structures de données et des fonctions au sein de la bibliothèque, elles sont définies au niveau du fichier et peuvent être directement importées et utilisées dans le contrat final. La troisième approche offre une meilleure interactivité homme-machine en utilisant des structures de données personnalisées, en appliquant des fonctions à la portée globale et en appliquant des opérateurs d'alias à certaines fonctions dans une mesure limitée.

Le compilateur propose deux passes d'optimisation. Le premier est l'optimiseur de niveau instruction, qui effectue des opérations d'optimisation sur le bytecode final. La seconde est l'ajout récent de l'utilisation du langage Yul (nous en parlerons plus tard) comme représentation intermédiaire (IR) pendant le processus de compilation, puis d'effectuer des opérations d'optimisation sur le code Yul généré.

Pour interagir avec les méthodes publiques et externes dans un contrat, Solidity spécifie une norme ABI (Application Binary Interface) pour interagir avec ses contrats. Actuellement, le Solidity ABI est considéré comme la norme de facto pour les DSL EVM. Les normes Ethereum ERC spécifiant les interfaces externes sont mises en œuvre conformément à la spécification ABI et au guide de style de Solidity. D'autres langages suivent également la spécification ABI de Solidity avec très peu d'écarts.

Solidity fournit également des blocs Yul en ligne, permettant un accès de bas niveau au jeu d'instructions EVM. Le bloc Yul contient un sous-ensemble de fonctionnalités Yul, voir la section Yul pour plus de détails. Ceci est généralement utilisé pour l'optimisation du gaz, en tirant parti des fonctionnalités non prises en charge par la syntaxe de niveau supérieur et en personnalisant le stockage, la mémoire et les données d'appel.

En raison de la popularité de Solidity, les outils de développement sont très matures et bien conçus, et Foundry se démarque à cet égard.

Ce qui suit est un contrat simple rédigé en Solidity :

Vyper

Vyper est un langage de haut niveau avec une syntaxe similaire à Python. C'est presque un sous-ensemble de Python avec quelques différences mineures. Il s'agit du deuxième DSL EVM le plus populaire. Vyper est optimisé pour la sécurité, la lisibilité, l'auditabilité et l'efficacité énergétique. Il n'utilise pas de modèles orientés objet, d'assemblage en ligne et ne prend pas en charge la réutilisation du code. Son compilateur est écrit en Python.

Les variables stockées dans le stockage persistant sont déclarées au niveau du fichier. Si leur valeur est connue au moment de la compilation, ils peuvent être déclarés comme « constants » ; si leur valeur est connue au moment du déploiement, ils peuvent être déclarés comme « immuables » s'ils sont marqués. S'il est public, le contrat final l'exposera ; une fonction en lecture seule pour la variable. Les valeurs des constantes et des invariants sont accessibles en interne via leurs noms, mais les variables mutables dans le stockage persistant sont accessibles en ajoutant « self » au nom. Ceci est utile pour éviter les conflits d'espace de noms entre les variables stockées, les paramètres de fonction et les variables locales.

Semblable à Solidity, Vyper utilise également des attributs de fonction pour représenter la visibilité et la variabilité des fonctions. Les fonctions marquées "@external" sont accessibles depuis des contrats externes via l'instruction "call". Les fonctions marquées « @internal » ne sont accessibles qu'au sein du même contrat et doivent être préfixées par « self ». Les fonctions marquées « @pure » ne peuvent pas lire les données de l'environnement d'exécution ou du stockage persistant, écrire sur le stockage persistant ou créer des effets secondaires. Les fonctions marquées « @view » peuvent lire les données de l'environnement d'exécution ou du stockage persistant, mais ne peuvent pas écrire sur le stockage persistant ni créer d'effets secondaires. Les fonctions marquées « @payable » peuvent lire ou écrire sur un stockage persistant, créer des effets secondaires et recevoir de l'ETH. Les fonctions qui ne déclarent pas cet attribut de mutabilité sont par défaut non payantes, c'est-à-dire qu'elles sont identiques aux fonctions payantes, mais ne peuvent pas recevoir d'ETH.

Le compilateur Vyper choisit également de stocker les variables locales en mémoire plutôt que sur la pile. Cela rend les contrats plus simples et plus efficaces, et résout le problème de « pile trop profonde » courant dans d'autres langages de haut niveau. Cependant, cela implique également certains compromis.

De plus, puisque la disposition de la mémoire doit être connue au moment de la compilation, la capacité maximale des types dynamiques doit également être connue au moment de la compilation, ce qui constitue une limitation. De plus, l'allocation de grandes quantités de mémoire peut entraîner une consommation de gaz non linéaire, comme mentionné dans la section de présentation de l'EVM. Cependant, pour de nombreux cas d’utilisation, ce coût du gaz est négligeable.

Bien que Vyper ne prenne pas en charge l'assemblage en ligne, il fournit davantage de fonctions intégrées pour garantir que presque toutes les fonctionnalités de Solidity et Yul sont également disponibles dans Vyper. Les opérations de bits de bas niveau, les appels externes et les opérations de contrat proxy sont accessibles via des fonctions intégrées, et des configurations de stockage personnalisées peuvent être implémentées en fournissant des fichiers de superposition au moment de la compilation.

Vyper ne dispose pas d'une suite riche d'outils de développement, mais il dispose d'outils plus étroitement intégrés et peut également se connecter aux outils de développement Solidity. Les outils Vyper notables incluent l'interpréteur Titanaboa, qui possède de nombreux outils intégrés liés à l'EVM et à Vyper pour l'expérimentation et le développement, et Dasy, un Lisp basé sur Vyper avec exécution de code au moment de la compilation.

Voici un contrat simple rédigé en Vyper :

Fe

Fe est un langage de haut niveau comme Rust qui est actuellement en développement actif, la plupart des fonctionnalités n'étant pas encore disponibles. Son compilateur est principalement écrit en Rust, mais utilise Yul comme représentation intermédiaire (IR), en s'appuyant sur l'optimiseur Yul écrit en C++. Cela devrait changer avec l’ajout de Sonatina, un backend natif de Rust. Fe utilise des modules pour le partage de code, il n'utilise donc pas de modèle orienté objet, mais réutilise le code via un système basé sur des modules, où les variables, les types et les fonctions sont déclarés dans les modules et peuvent être importés d'une manière similaire à Rust.

Les variables de stockage persistantes sont déclarées au niveau du contrat et ne sont pas accessibles publiquement sans fonctions getter définies manuellement. Les constantes peuvent être déclarées au niveau du fichier ou du module et accessibles à l'intérieur du contrat. Les variables de temps de déploiement immuables ne sont actuellement pas prises en charge.

Les méthodes peuvent être déclarées au niveau du module ou dans un contrat, et sont pures et privées par défaut. Pour rendre publique une méthode de contrat, il faut ajouter le mot-clé "pub" avant la définition, ce qui la rend accessible en externe. Pour lire à partir d'une variable de stockage persistante, le premier paramètre de la méthode doit être "self". Ajoutez "self" avant le nom de la variable pour donner à la méthode un accès en lecture seule à la variable de stockage locale. Pour lire et écrire sur un stockage persistant, le premier paramètre doit être "mut self". Le mot-clé "mut" indique que le stockage du contrat est modifiable lors de l'exécution de la méthode. L'accès aux variables d'environnement se fait en passant le paramètre "Context" à la méthode, généralement nommée "ctx".

Les fonctions et les types personnalisés peuvent être déclarés au niveau du module. Par défaut, les éléments du module sont privés et ne sont accessibles que si le mot-clé "pub" est utilisé. Cependant, à ne pas confondre avec le mot-clé « pub » au niveau du contrat. Les membres publics d'un module ne sont accessibles que dans le contrat final ou dans d'autres modules.

Fe ne prend actuellement pas en charge l'assemblage en ligne, mais les instructions sont encapsulées par des éléments intrinsèques du compilateur ou des fonctions spéciales qui se résolvent en instructions au moment de la compilation.

Fe suit la syntaxe et le système de types de Rust, prenant en charge les alias de types, les énumérations avec sous-types, traits et génériques. La prise en charge de cette fonctionnalité est actuellement limitée, mais elle est en cours d'élaboration. Les traits peuvent être définis et implémentés pour différents types, mais les génériques et les contraintes de traits ne sont pas pris en charge. Les énumérations prennent en charge les sous-types et les méthodes peuvent y être implémentées, mais elles ne peuvent pas être codées dans des fonctions externes. Bien que le système de types de Fe soit encore en cours de développement, il présente un grand potentiel pour l'écriture de code plus sûr et vérifié au moment de la compilation pour les développeurs.

Voici un contrat simple écrit en Fe :

Souffler

Huff est un langage assembleur avec contrôle manuel de la pile et abstraction minimale du jeu d'instructions EVM. Grâce à la directive "#include", tous les fichiers Huff inclus peuvent être analysés lors de la compilation pour permettre la réutilisation du code. Initialement écrit par l'équipe Aztec pour des algorithmes de courbe elliptique extrêmement optimisés, le compilateur a ensuite été réécrit en TypeScript puis en Rust.

Les constantes doivent être définies au moment de la compilation, les immuables ne sont actuellement pas pris en charge et les variables de stockage persistantes ne sont pas explicitement définies dans le langage. Étant donné que les variables de stockage nommées sont une abstraction de haut niveau, l'écriture sur le stockage persistant dans Huff se fait via les opcodes "sstore" pour l'écriture et "sload" pour la lecture. Des dispositions de stockage personnalisées peuvent être définies par l'utilisateur, ou vous pouvez suivre la convention consistant à partir de zéro et à incrémenter chaque variable à l'aide du "FREE_STORAGE_POINTER" intégré au compilateur. Rendre une variable stockée accessible de l'extérieur nécessite de définir manuellement un chemin de code capable de lire et de renvoyer la variable à l'appelant.

Les fonctions externes sont également des abstractions introduites par les langages de haut niveau, il n'y a donc pas de concept de fonctions externes dans Huff. Cependant, la plupart des projets suivent les spécifications ABI d'autres langages de haut niveau à des degrés divers, le plus souvent Solidity. Un modèle courant consiste à définir un « répartiteur » qui charge les données d'appel brutes et les utilise pour vérifier la correspondance des sélecteurs de fonctions. S'il correspond, son code suivant est exécuté. Étant donné que les planificateurs sont définis par l'utilisateur, ils peuvent suivre différents modèles de planification. Solidity trie les sélecteurs dans ses planificateurs par ordre alphabétique par nom, Vyper trie numériquement et effectue une recherche binaire au moment de l'exécution, et la plupart des planificateurs Huff trient par fréquence d'utilisation attendue des fonctions, utilisant rarement des tables de saut. Actuellement, les tables de sauts ne sont pas prises en charge nativement dans l'EVM, donc des instructions d'introspection telles que « codecopy » doivent être utilisées pour les implémenter.

Les fonctions internes sont définies à l'aide de la directive #definefn", qui peut accepter des paramètres de modèle pour une flexibilité accrue et spécifier la profondeur de pile attendue au début et à la fin de la fonction. Ces fonctions étant internes, elles ne sont pas accessibles de l'extérieur. L'accès interne nécessite l'utilisation de l'instruction "jump".

D'autres flux de contrôle, tels que les instructions conditionnelles et les instructions de boucle, peuvent utiliser des définitions de cibles de saut. La cible du saut est définie par un identifiant suivi de deux points. Des sauts vers ces cibles peuvent être effectués en poussant l'identifiant sur la pile et en exécutant une instruction de saut. Ceci est résolu en un décalage de bytecode au moment de la compilation.

Les macros sont définies par #definemacro" et sont par ailleurs identiques aux fonctions internes. La principale différence est que la macro ne génère pas d'instruction de « saut » au moment de la compilation, mais copie le corps de la macro directement dans chaque appel du fichier.

Cette conception compromet la réduction des sauts arbitraires par rapport au coût du gaz d'exécution au détriment de l'augmentation de la taille du code lorsqu'il est appelé plusieurs fois. La macro "MAIN" est considérée comme le point d'entrée du contrat, et la première instruction de son corps deviendra la première instruction du bytecode d'exécution.

Les autres fonctionnalités intégrées au compilateur incluent la génération de hachage d'événements pour la journalisation, les sélecteurs de fonctions pour la planification, les sélecteurs d'erreurs pour la gestion des erreurs et les vérificateurs de taille de code pour les fonctions et macros internes.

Remarque : Les commentaires de pile tels que "//[count]" ne sont pas obligatoires, ils servent uniquement à indiquer l'état de la pile à la fin de l'exécution de la ligne.

Voici un contrat simple écrit en Huff :

ETK

L'EVM Toolkit (ETK) est un langage assembleur avec une gestion manuelle de la pile et des abstractions minimales. Le code peut être réutilisé via les directives "%include" et "%import", et le compilateur est écrit en Rust.

Une différence significative entre Huff et ETK est que Huff ajoute une légère abstraction au code d'initialisation, également connu sous le nom de code constructeur, qui peut être remplacé en définissant une macro spéciale "CONSTRUCTOR". Dans ETK, ceux-ci ne sont pas abstraits, le code d'initialisation et le code d'exécution doivent être définis ensemble.

Semblable à Huff, ETK lit et écrit le stockage persistant via les instructions « sload » et « sstore ». Cependant, il n'existe pas de mot-clé constant ou immuable, mais les constantes peuvent être simulées à l'aide de l'une des deux macros d'ETK, la macro d'expression. Les macros d'expression ne se résolvent pas en instructions, mais génèrent à la place des valeurs numériques qui peuvent être utilisées dans d'autres instructions. Par exemple, il se peut qu'il ne génère pas entièrement la commande « push », mais il peut générer un nombre à inclure dans la commande « push ».

Comme mentionné précédemment, les fonctions externes sont un concept de langage de haut niveau, donc exposer le chemin du code en externe nécessite la création d'un planificateur de sélecteur de fonctions.

Les fonctions internes ne sont pas explicitement définies comme dans d'autres langages. Au lieu de cela, des alias définis par l'utilisateur peuvent être donnés pour accéder aux cibles et y accéder par leurs noms. Cela permet également d'autres flux de contrôle tels que des boucles et des instructions conditionnelles.

ETK prend en charge deux types de macros. La première est une macro d'expression qui accepte n'importe quel nombre d'arguments et renvoie une valeur numérique pouvant être utilisée dans d'autres instructions. Les macros d'expression ne génèrent pas d'instructions, mais génèrent plutôt des valeurs ou des constantes immédiates. Cependant, les macros de directives acceptent n'importe quel nombre d'arguments et génèrent n'importe quel nombre de directives au moment de la compilation. Les macros d'instructions dans ETK sont similaires aux macros Huff.

Ce qui suit est un simple contrat rédigé en ETK :

Yul

Yul est un langage assembleur avec un flux de contrôle de haut niveau et un grand nombre d'abstractions. Il fait partie de la chaîne d'outils Solidity et peut éventuellement être utilisé dans les passes de construction Solidity. Yul ne prend pas en charge la réutilisation du code car il est destiné à être une cible de compilation plutôt qu'un langage autonome. Son compilateur est écrit en C++ et il est prévu de le migrer vers Rust avec le reste du canal Solidity.

Dans Yul, le code est divisé en objets, qui peuvent contenir du code, des données et des objets imbriqués. Par conséquent, il n’y a pas de constantes ou de fonctions externes dans Yul. Les répartiteurs de sélecteurs de fonctions doivent être définis pour exposer les chemins de code au monde extérieur.

À l'exception des instructions de pile et de flux de contrôle, la plupart des instructions sont exposées en tant que fonctions dans Yul. Les instructions peuvent être imbriquées pour raccourcir la longueur du code, ou affectées à des variables temporaires, puis transmises à d'autres instructions à utiliser. Les branches conditionnelles peuvent utiliser un bloc "if", qui est exécuté si la valeur est différente de zéro, mais il n'y a pas de bloc "else", donc la gestion de plusieurs chemins de code nécessite l'utilisation d'un "commutateur" pour gérer un nombre quelconque de cas et une option de secours « par défaut ». Les boucles peuvent être exécutées à l'aide d'une boucle « for » ; bien que sa syntaxe soit différente des autres langages de haut niveau, elle fournit les mêmes fonctionnalités de base. Les fonctions internes peuvent être définies à l'aide du mot-clé « function » et sont similaires aux définitions de fonctions dans les langages de haut niveau.

La plupart des fonctionnalités de Yul sont exposées dans Solidity à l'aide de blocs d'assemblage en ligne. Cela permet aux développeurs de briser les abstractions et d'écrire des fonctionnalités personnalisées ou d'utiliser Yul dans des fonctionnalités non disponibles dans la syntaxe de haut niveau. Cependant, l'utilisation de cette fonctionnalité nécessite une compréhension approfondie du comportement de Solidity en ce qui concerne les données d'appel, la mémoire et le stockage.

Il existe également des fonctions uniques. Les fonctions "datasize", "dataoffset" et "datacopy" opèrent sur les objets Yul via leurs alias de chaîne. Les fonctions "setimmutable" et "loadimmutable" permettent de définir et de charger des paramètres immuables dans le constructeur, bien que leur utilisation soit restreinte. La fonction "memoryguard" indique que seule une plage de mémoire donnée est allouée, permettant au compilateur d'utiliser la mémoire au-delà de la plage protégée pour des optimisations supplémentaires. Enfin, "verbatim" permet d'utiliser des instructions inconnues du compilateur Yul.

Voici un contrat simple écrit en Yul :

Caractéristiques d'un bon EVM DSL

Un bon DSL EVM doit apprendre des avantages et des inconvénients de chaque langage répertorié ici, et doit également inclure les bases trouvées dans presque tous les langages modernes, telles que les conditions, la correspondance de modèles, les boucles, les fonctions, etc. Le code doit être explicite, avec un minimum d'abstractions implicites ajoutées pour des raisons de beauté ou de lisibilité du code. Dans des environnements à enjeux élevés et dont l’exactitude est critique, chaque ligne de code doit être interprétable sans ambiguïté. De plus, un système de modules bien défini devrait être au cœur de tout grand langage. Il doit indiquer clairement quels éléments sont définis dans quelle portée et lesquels sont accessibles. Chaque élément d'un module doit être privé par défaut, et seuls les éléments explicitement publics doivent être accessibles publiquement en externe.

Dans un environnement aux ressources limitées comme EVM, l’efficacité est importante. L'efficacité est souvent obtenue en fournissant des abstractions à faible coût telles que l'exécution de code au moment de la compilation via des macros, un système de types riche pour créer des bibliothèques réutilisables bien conçues et des wrappers pour les interactions courantes en chaîne. Les macros génèrent du code au moment de la compilation, ce qui est utile pour réduire le code passe-partout pour les opérations courantes, et dans des cas comme celui de Huff où il peut être utilisé pour comparer la taille du code par rapport à l'efficacité d'exécution. Un système de types riche permet un code plus expressif, davantage de vérifications au moment de la compilation pour détecter les erreurs avant l'exécution et, lorsqu'il est combiné avec des éléments intrinsèques du compilateur vérifiés par type, peut éliminer le besoin d'une grande partie de l'assemblage en ligne. Les génériques permettent également aux valeurs nullables (telles que le code externe) d'être encapsulées dans des types « option », ou aux opérations sujettes aux erreurs (telles que les appels externes) d'être encapsulées dans des types « résultat ». Ces deux types sont des exemples de la manière dont les rédacteurs de bibliothèques obligent les développeurs à gérer chaque résultat en définissant des chemins de code ou des transactions qui récupèrent les résultats ayant échoué. Cependant, gardez à l’esprit qu’il s’agit d’abstractions au moment de la compilation qui se résolvent en de simples sauts conditionnels au moment de l’exécution. Forcer les développeurs à gérer chaque résultat au moment de la compilation augmente le temps de développement initial, mais l'avantage est qu'il y a beaucoup moins de surprises au moment de l'exécution.

La flexibilité est également importante pour les développeurs. Ainsi, même si la solution par défaut pour les opérations complexes devrait être la voie la plus sûre et potentiellement la moins efficace, il est parfois nécessaire d'utiliser des chemins de code plus efficaces ou des fonctionnalités non prises en charge. Pour ce faire, l’assemblage en ligne doit être ouvert aux développeurs, sans garde-fou. L'assemblage en ligne de Solidity définit des garde-fous pour plus de simplicité et de meilleures passes d'optimisation, mais lorsque les développeurs ont besoin d'un contrôle total sur l'environnement d'exécution, ces droits doivent leur être accordés.

Certaines fonctionnalités potentiellement utiles incluent la possibilité de manipuler les propriétés des fonctions et d'autres éléments au moment de la compilation. Par exemple, l'attribut « inline » peut copier le corps d'une fonction simple dans chaque appel, plutôt que de créer davantage de sauts pour plus d'efficacité. L'attribut "abi" permet de remplacer manuellement l'ABI généré par une fonction externe donnée pour s'adapter aux langages avec des styles de codage différents. De plus, un planificateur de fonctions facultatif peut être défini, permettant une personnalisation au sein du langage de haut niveau pour des optimisations supplémentaires sur les chemins de code susceptibles d'être plus couramment utilisés. Par exemple, vérifiez si le sélecteur est "transfer" ou "transferFrom" avant d'exécuter "name".

en conclusion

La conception EVM DSL a encore un long chemin à parcourir. Chaque langage a ses propres décisions de conception, et j'ai hâte de voir comment elles évolueront à l'avenir. En tant que développeurs, il est dans notre intérêt d’apprendre autant de langues que possible. Premièrement, apprendre plusieurs langages et comprendre leurs différences et similitudes approfondira notre compréhension de la programmation et de l’architecture machine sous-jacente. Deuxièmement, la langue a de profonds effets de réseau et de fortes propriétés de rétention. Il ne fait aucun doute que les grands acteurs créent leurs propres langages de programmation, de C#, Swift et Kotlin à Solidity, Sway et Cairo. Apprendre à basculer de manière transparente entre ces langages offre une flexibilité inégalée pour une carrière en génie logiciel. Enfin, il est important de comprendre qu’il y a beaucoup de travail derrière chaque langue. Personne n'est parfait, mais d'innombrables personnes talentueuses déploient beaucoup d'efforts pour créer des expériences sûres et agréables pour les développeurs comme nous.