Además de Solidity, ¿a qué otros lenguajes EVM vale la pena prestarle atención?
Escrito por: jtriley.ethjtriley.eth
Compilado por: 0x11, Foresight News
La máquina virtual Ethereum (EVM) es una máquina Turing de 256 bits, basada en pilas y accesible globalmente. Debido a que la arquitectura es significativamente diferente de otras máquinas virtuales y físicas, EVM requiere un lenguaje DSL específico de dominio (nota: un lenguaje específico de dominio se refiere a un lenguaje informático que se centra en un determinado dominio de aplicación).
En este artículo, veremos lo último en diseño de EVM DSL, abarcando seis lenguajes: Solidity, Vyper, Fe, Huff, Yul y ETK.
Versión de idioma
Solidez: 0.8.19
Viper: 0.3.7
Hierro: 0,21,0
Suspiro: 0.3.1
ETK: 0.2.1
Año: 0.8.19
Leer este artículo requiere que tenga conocimientos básicos de EVM, pila y programación.
Descripción general de la máquina virtual Ethereum
La EVM es una máquina de Turing basada en una pila de 256 bits. Sin embargo, antes de profundizar en su compilador, conviene introducir algunas características funcionales.
Debido a que el EVM está "Turing completo", sufrirá el "problema de detención". En resumen, antes de que se ejecute un programa, no hay forma de determinar si finalizará en el futuro. La forma en que el EVM resuelve este problema es medir unidades de cálculo mediante "Gas", que generalmente es proporcional a los recursos físicos necesarios para ejecutar instrucciones. La cantidad de Gas por transacción es limitada y el iniciador de la transacción debe pagar ETH proporcional al Gas consumido por la transacción. Un impacto de esta estrategia es que si hay dos contratos inteligentes funcionalmente idénticos, se adoptará más el que consuma menos gas. Esto da como resultado protocolos que compiten por una eficiencia extrema del gas, y los ingenieros se esfuerzan por minimizar el consumo de gas para tareas específicas.
Además, cuando se llama a un contrato, se crea un contexto de ejecución. En este contexto, el contrato tiene una pila para operaciones y procesamiento, una instancia de memoria lineal para lectura y escritura, un almacenamiento local persistente para lectura y escritura del contrato, y los datos adjuntos a la llamada "calldata" se pueden leer pero no escribir. .
Una nota importante sobre la memoria es que, si bien no existe un "límite superior" definido para su tamaño, sigue siendo limitado. El costo del gas para expandir la memoria es dinámico: una vez que se alcanza un umbral, el costo de expandir la memoria aumenta cuadráticamente, es decir, el costo del gas es proporcional al cuadrado de la asignación de memoria adicional.
Los contratos también pueden llamar a otros contratos utilizando varias instrucciones diferentes. La instrucción "llamar" envía datos y ETH opcional al contrato de destino, luego crea su propio contexto de ejecución hasta que se detiene la ejecución del contrato de destino. La directiva "staticcall" es la misma que "call", pero agrega una verificación que afirma que ninguna parte del estado global se ha actualizado antes de que se complete la llamada estática. Finalmente, la directiva "delegatecall" se comporta como "call", excepto que retiene cierta información ambiental del contexto anterior. Normalmente se utiliza para bibliotecas externas y contratos de proxy.
Por qué es importante el diseño del lenguaje
Los lenguajes específicos de dominio (DSL) son necesarios cuando se interactúa con arquitecturas atípicas. Si bien existen cadenas de herramientas de compilación como LLVM, confiar en ellas para manejar contratos inteligentes no es nada ideal en situaciones donde la corrección del programa y la eficiencia computacional son críticas.
La corrección del programa es importante porque los contratos inteligentes son inmutables de forma predeterminada y son una opción popular para aplicaciones financieras dadas las propiedades de las máquinas virtuales (VM) blockchain. Si bien existe una solución actualizable para EVM, en el mejor de los casos es un parche y, en el peor, una vulnerabilidad de ejecución de código arbitrario.
La eficiencia computacional también es crítica, ya que minimizar el cálculo tiene ventajas económicas, pero no a expensas de la seguridad.
En resumen, un EVM DSL debe equilibrar la corrección del programa y la eficiencia del gas, logrando uno u otro haciendo diferentes concesiones sin sacrificar demasiada flexibilidad.
Descripción general del idioma
Para cada idioma, describiremos sus características destacadas y opciones de diseño, e incluiremos un contrato inteligente con función de conteo simple. La popularidad verbal se determina en función de los datos del valor total bloqueado (TVL) en Defi Llama.
Solidez
Solidity es un lenguaje de alto nivel cuya sintaxis es similar a C, Java y Javascript. Es el idioma más popular según TVL, con un TVL diez veces mayor que el del siguiente idioma más popular. Para la reutilización de código, utiliza un patrón orientado a objetos, donde los contratos inteligentes se tratan como objetos de clase, aprovechando la herencia múltiple. El compilador está escrito en C++ y hay planes para migrar a Rust en el futuro.
Los campos de contrato mutables se almacenan en un almacenamiento persistente a menos que sus valores se conozcan en el momento de la compilación (constantes) o en el momento de la implementación (inmutables). Los métodos declarados dentro de un contrato pueden ser declarados puros, visibles, pagaderos o no pagaderos por defecto pero con estatus modificable. Los métodos puros no leen datos del entorno de ejecución y no pueden leer ni escribir en el almacenamiento persistente, es decir, dada la misma entrada, los métodos puros siempre devolverán el mismo resultado y no tendrán efectos secundarios. Los métodos de visualización pueden leer datos del almacén de persistencia o del entorno de ejecución, pero no pueden escribir en el almacén de persistencia ni pueden crear efectos secundarios como agregar registros de transacciones. Los métodos pagaderos pueden leer y escribir almacenamiento persistente, leer datos del entorno de ejecución, producir efectos secundarios y recibir ETH adjunto a la llamada. El método no pagable es el mismo que el método pagadero, pero tiene una verificación de tiempo de ejecución para afirmar que no hay ETH adjunto en el contexto de ejecución actual.
NOTA: Adjuntar ETH a una transacción es independiente del pago de tarifas de gas; el contrato recibe el ETH adjunto y usted puede optar por aceptarlo o rechazarlo restaurando el contexto.
Cuando se declaran dentro del alcance de un contrato, los métodos pueden especificar cuatro modificadores de visibilidad: privado, interno, público o externo. Se puede acceder a los métodos privados internamente a través de la instrucción "saltar" dentro del contrato actual. Ningún contrato heredado puede acceder directamente a métodos privados. También se puede acceder a los métodos internos mediante la instrucción "saltar", pero los contratos heredados pueden utilizar métodos internos directamente. Se puede acceder a los métodos públicos mediante contratos externos a través de la instrucción "llamar", que crea un nuevo contexto de ejecución, e internamente mediante saltos al llamar al método directamente. También se puede acceder a los métodos públicos desde el mismo contrato en un nuevo contexto de ejecución anteponiendo "this" a la llamada al método. Solo se puede acceder a los métodos externos a través de la instrucción "llamar". Ya sean de diferentes contratos o dentro del mismo contrato, "esto debe agregarse antes de la llamada al método".
Nota: La instrucción "saltar" opera el contador del programa y la instrucción "llamar" crea un nuevo contexto de ejecución durante la ejecución del contrato de destino. Cuando sea posible, utilice "saltar" en lugar de "llamar" para ahorrar gasolina.
Solidity también proporciona tres formas de definir bibliotecas. La primera es una biblioteca externa, que es un contrato sin estado que se implementa por separado en la cadena, se vincula dinámicamente cuando se llama al contrato y se accede a él a través de la instrucción "delegatecall". Este es el enfoque menos común porque el soporte de herramientas para bibliotecas externas es insuficiente, la "llamada delegada" es costosa, debe cargar código adicional desde el almacenamiento persistente y requiere múltiples transacciones para su implementación. Las bibliotecas internas se definen de la misma manera que las bibliotecas externas, excepto que cada método debe definirse como un método interno. En el momento de la compilación, la biblioteca interna se integra en el contrato final y, durante la fase de análisis del código inactivo, se eliminan los métodos no utilizados de la biblioteca. La tercera forma es similar a la biblioteca interna, pero en lugar de definir estructuras de datos y funciones dentro de la biblioteca, se definen a nivel de archivo y se pueden importar y utilizar directamente en el contrato final. El tercer enfoque proporciona una mejor interactividad entre humanos y computadoras mediante el uso de estructuras de datos personalizadas, la aplicación de funciones al alcance global y la aplicación de operadores de alias a ciertas funciones de forma limitada.
El compilador proporciona dos pasos de optimización. El primero es el optimizador a nivel de instrucción, que realiza operaciones de optimización en el código de bytes final. El segundo es la reciente incorporación del uso del lenguaje Yul (más sobre esto más adelante) como una representación intermedia (IR) durante el proceso de compilación y luego realizar operaciones de optimización en el código Yul generado.
Para interactuar con métodos públicos y externos en un contrato, Solidity especifica un estándar de Interfaz Binaria de Aplicación (ABI) para interactuar con sus contratos. Actualmente, Solidity ABI se considera el estándar de facto para EVM DSL. Los estándares Ethereum ERC que especifican interfaces externas se implementan de acuerdo con la especificación ABI y la guía de estilo de Solidity. Otros lenguajes también siguen la especificación ABI de Solidity con muy pocas desviaciones.
Solidity también proporciona bloques Yul en línea, lo que permite el acceso de bajo nivel al conjunto de instrucciones EVM. El bloque Yul contiene un subconjunto de la funcionalidad Yul; consulte la sección Yul para obtener más detalles. Esto generalmente se usa para optimizar el gas, aprovechar funciones que no son compatibles con la sintaxis de nivel superior y personalizar el almacenamiento, la memoria y los datos de llamadas.
Debido a la popularidad de Solidity, las herramientas de desarrollo son muy maduras y están bien diseñadas, y Foundry se destaca en este sentido.
El siguiente es un contrato simple escrito en Solidity:

Víbora
Vyper es un lenguaje de alto nivel con una sintaxis similar a Python. Es casi un subconjunto de Python con algunas diferencias menores. Es el segundo EVM DSL más popular. Vyper está optimizado para seguridad, legibilidad, auditabilidad y eficiencia de gas. No utiliza patrones orientados a objetos, ensamblaje en línea y no admite la reutilización de código. Su compilador está escrito en Python.
Las variables almacenadas en almacenamiento persistente se declaran a nivel de archivo. Si su valor se conoce en el momento de la compilación, se pueden declarar como "constantes"; si se conoce su valor en el momento de la implementación, se pueden declarar como "inmutables" si están marcados. Si es público, el contrato final lo expondrá; una función de solo lectura para la variable. Se accede a los valores de constantes e invariantes internamente a través de sus nombres, pero se puede acceder a las variables mutables en el almacenamiento persistente anteponiendo "self" al nombre. Esto es útil para evitar conflictos de espacios de nombres entre variables almacenadas, parámetros de funciones y variables locales.
Al igual que Solidity, Vyper también utiliza atributos de funciones para representar la visibilidad y variabilidad de las funciones. Se puede acceder a las funciones marcadas como "@external" desde contratos externos a través de la instrucción "call". Solo se puede acceder a las funciones marcadas como "@internal" dentro del mismo contrato y deben tener el prefijo "self". Las funciones marcadas como "@pure" no pueden leer datos del entorno de ejecución o del almacenamiento persistente, escribir en el almacenamiento persistente ni crear efectos secundarios. Las funciones marcadas como "@view" pueden leer datos del entorno de ejecución o del almacenamiento persistente, pero no pueden escribir en el almacenamiento persistente ni crear efectos secundarios. Las funciones marcadas como "@payable" pueden leer o escribir en almacenamiento persistente, crear efectos secundarios y recibir ETH. Las funciones que no declaran este atributo de mutabilidad por defecto son no pagaderas, es decir, son iguales a las funciones pagaderas, pero no pueden recibir ETH.
El compilador de Vyper también elige almacenar variables locales en la memoria en lugar de en la pila. Esto hace que los contratos sean más simples y eficientes, y resuelve el problema de la "pila demasiado profunda" común en otros lenguajes de alto nivel. Sin embargo, esto también conlleva algunas compensaciones.
Además, dado que se debe conocer el diseño de la memoria en el momento de la compilación, la capacidad máxima de los tipos dinámicos también se debe conocer en el momento de la compilación, lo cual es una limitación. Además, la asignación de grandes cantidades de memoria puede generar un consumo de gas no lineal, como se menciona en la sección de descripción general de EVM. Sin embargo, para muchos casos de uso, este costo del gas es insignificante.
Si bien Vyper no admite el ensamblaje en línea, proporciona más funciones integradas para garantizar que casi todas las funciones de Solidity y Yul también estén disponibles en Vyper. Se puede acceder a operaciones de bits de bajo nivel, llamadas externas y operaciones de contratos de proxy a través de funciones integradas, y se pueden implementar diseños de almacenamiento personalizados proporcionando archivos superpuestos en el momento de la compilación.
Vyper no tiene un conjunto rico de herramientas de desarrollo, pero sí tiene herramientas que están más estrechamente integradas y también pueden conectarse a las herramientas de desarrollo de Solidity. Las herramientas notables de Vyper incluyen el intérprete Titanaboa, que tiene muchas herramientas integradas relacionadas con EVM y Vyper para experimentación y desarrollo, y Dasy, un Lisp basado en Vyper con ejecución de código en tiempo de compilación.
Aquí hay un contrato simple escrito en Vyper:

Fé
Fe es un lenguaje de alto nivel como Rust que actualmente se encuentra en desarrollo activo y la mayoría de las funciones aún no están disponibles. Su compilador está escrito principalmente en Rust, pero utiliza Yul como representación intermedia (IR), basándose en el optimizador Yul escrito en C++. Se espera que esto cambie con la incorporación de Sonatina, un backend nativo de Rust. Fe usa módulos para compartir código, por lo que no usa un patrón orientado a objetos, sino que reutiliza el código a través de un sistema basado en módulos, donde las variables, tipos y funciones se declaran dentro de los módulos y se pueden importar de manera similar a Rust.
Las variables de almacenamiento persistentes se declaran a nivel de contrato y no son accesibles públicamente sin funciones getter definidas manualmente. Las constantes pueden declararse a nivel de archivo o módulo y ser accesibles dentro del contrato. Actualmente no se admiten variables de tiempo de implementación inmutables.
Los métodos se pueden declarar a nivel de módulo o dentro de un contrato y son puros y privados de forma predeterminada. Para hacer público un método de contrato, se debe agregar la palabra clave "pub" antes de la definición, lo que lo hace accesible externamente. Para leer desde una variable de almacenamiento persistente, el primer parámetro del método debe ser "self". Agregue "self" antes del nombre de la variable para darle al método acceso de solo lectura a la variable de almacenamiento local. Para leer y escribir en un almacenamiento persistente, el primer parámetro debe ser "mut self". La palabra clave "mut" indica que el almacenamiento del contrato es mutable durante la ejecución del método. El acceso a las variables de entorno se logra pasando el parámetro "Contexto" al método, generalmente llamado "ctx".
Las funciones y los tipos personalizados se pueden declarar a nivel de módulo. De forma predeterminada, los elementos del módulo son privados y no se puede acceder a ellos a menos que se utilice la palabra clave "pub". Sin embargo, no debe confundirse con la palabra clave "pub" a nivel de contrato. Solo se puede acceder a los miembros públicos de un módulo dentro del contrato final u otros módulos.
Actualmente, Fe no admite el ensamblaje en línea; en cambio, las instrucciones están empaquetadas por elementos intrínsecos del compilador o funciones especiales que se resuelven en instrucciones en el momento de la compilación.
Fe sigue la sintaxis y el sistema de tipos de Rust, admitiendo alias de tipos, enumeraciones con subtipos, rasgos y genéricos. El soporte para esto es actualmente limitado, pero se está trabajando en ello. Los rasgos se pueden definir e implementar para diferentes tipos, pero no se admiten restricciones genéricas ni de rasgos. Las enumeraciones admiten subtipos y se pueden implementar métodos en ellas, pero no se pueden codificar en funciones externas. Aunque el sistema de tipos de Fe todavía es un trabajo en progreso, muestra un gran potencial para escribir código más seguro y verificado en tiempo de compilación para los desarrolladores.
Aquí hay un contrato simple escrito en Fe:

Rabieta
Huff es un lenguaje ensamblador con control manual de pila y una abstracción mínima del conjunto de instrucciones EVM. A través de la directiva "#include", cualquier archivo Huff incluido se puede analizar durante la compilación para lograr la reutilización del código. Escrito originalmente por el equipo azteca para algoritmos de curva elíptica extremadamente optimizados, el compilador fue luego reescrito en TypeScript y luego en Rust.
Las constantes deben definirse en el momento de la compilación, actualmente no se admiten elementos inmutables y las variables de almacenamiento persistentes no están definidas explícitamente en el lenguaje. Dado que las variables de almacenamiento con nombre son una abstracción de alto nivel, la escritura en el almacenamiento persistente en Huff se realiza mediante los códigos de operación "sstore" para escribir y "sload" para leer. El usuario puede definir diseños de almacenamiento personalizados, o puede seguir la convención de comenzar desde cero e incrementar cada variable utilizando el "FREE_STORAGE_POINTER" integrado del compilador. Hacer que una variable almacenada sea accesible externamente requiere definir manualmente una ruta de código que pueda leer y devolver la variable a la persona que llama.
Las funciones externas también son abstracciones introducidas por lenguajes de alto nivel, por lo que no existe el concepto de funciones externas en Huff. Sin embargo, la mayoría de los proyectos siguen las especificaciones ABI de otros lenguajes de alto nivel en diversos grados, más comúnmente Solidity. Un patrón común es definir un "despachador" que carga los datos de llamadas sin procesar y los usa para verificar si hay selectores de funciones coincidentes. Si coincide, se ejecuta su código posterior. Dado que los programadores están definidos por el usuario, pueden seguir diferentes patrones de programación. Solidity ordena los selectores en sus programadores alfabéticamente por nombre, Vyper ordena numéricamente y realiza una búsqueda binaria en tiempo de ejecución, y la mayoría de los programadores Huff ordenan por frecuencia de uso esperada de la función, rara vez usando tablas de salto. Actualmente, las tablas de salto no son compatibles de forma nativa con EVM, por lo que es necesario utilizar instrucciones de introspección como "codecopy" para implementarlas.
Las funciones internas se definen mediante la directiva #definefn", que puede aceptar parámetros de plantilla para mayor flexibilidad y especificar la profundidad de pila esperada al principio y al final de la función. Dado que estas funciones son internas, no se puede acceder a ellas desde el exterior. El acceso interno requiere el uso de la instrucción "saltar".
Otros flujos de control, como declaraciones condicionales y declaraciones de bucle, pueden utilizar definiciones de destino de salto. El objetivo del salto se define mediante un identificador seguido de dos puntos. Se pueden realizar saltos a estos objetivos empujando el identificador a la pila y ejecutando una instrucción de salto. Esto se resuelve en un desplazamiento de código de bytes en el momento de la compilación.
Las macros se definen mediante #definemacro" y, por lo demás, son las mismas que las funciones internas. La diferencia clave es que la macro no genera una instrucción de "salto" en el momento de la compilación, sino que copia el cuerpo de la macro directamente en cada llamada del archivo.
Este diseño compensa la reducción de los saltos arbitrarios con el costo del gas en tiempo de ejecución a expensas de un mayor tamaño del código cuando se llama varias veces. La macro "PRINCIPAL" se considera el punto de entrada al contrato, y la primera instrucción en su cuerpo se convertirá en la primera instrucción en el código de bytes en tiempo de ejecución.
Otras características integradas en el compilador incluyen generación de hash de eventos para registro, selectores de funciones para programación, selectores de errores para manejo de errores y verificadores de tamaño de código para funciones internas y macros.
Nota: Los comentarios de la pila como "//[count]" no son necesarios, solo se utilizan para indicar el estado de la pila al final de la ejecución de la línea.
Aquí hay un contrato simple escrito en Huff:

ETK
EVM Toolkit (ETK) es un lenguaje ensamblador con gestión manual de pila y abstracciones mínimas. El código se puede reutilizar mediante las directivas "%include" y "%import", y el compilador está escrito en Rust.
Una diferencia significativa entre Huff y ETK es que Huff agrega una ligera abstracción al código de inicio, también conocido como código constructor, que puede anularse definiendo una macro especial "CONSTRUCTOR". En ETK, estos no están abstraídos, el código de inicio y el código de tiempo de ejecución deben definirse juntos.
Al igual que Huff, ETK lee y escribe almacenamiento persistente mediante las instrucciones "cargar" y "almacenar". Sin embargo, no existe una palabra clave constante o inmutable, pero las constantes se pueden simular usando una de las dos macros en ETK, la macro de expresión. Las macros de expresión no se resuelven en instrucciones, sino que generan valores numéricos que se pueden usar en otras instrucciones. Por ejemplo, es posible que no genere el comando "empujar" por completo, pero puede generar un número para incluirlo en el comando "empujar".
Como se mencionó anteriormente, las funciones externas son un concepto de lenguaje de alto nivel, por lo que exponer la ruta del código externamente requiere la creación de un programador selector de funciones.
Las funciones internas no están definidas explícitamente como en otros lenguajes. En cambio, se pueden asignar alias definidos por el usuario para saltar a los objetivos y saltar a ellos por sus nombres. Esto también permite otros flujos de control, como bucles y declaraciones condicionales.
ETK admite dos tipos de macros. La primera es una macro de expresión que acepta cualquier número de argumentos y devuelve un valor numérico que se puede utilizar en otras instrucciones. Las macros de expresión no generan instrucciones, sino que generan valores inmediatos o constantes. Sin embargo, las macros de directivas aceptan cualquier número de argumentos y generan cualquier número de directivas en tiempo de compilación. Las macros de instrucciones en ETK son similares a las macros de Huff.
El siguiente es un contrato simple escrito en ETK:

Yul
Yul es un lenguaje ensamblador con flujo de control de alto nivel y una gran cantidad de abstracciones. Es parte de la cadena de herramientas de Solidity y, opcionalmente, se puede utilizar en pases de construcción de Solidity. Yul no admite la reutilización de código porque está destinado a ser un objetivo de compilación en lugar de un lenguaje independiente. Su compilador está escrito en C++ y hay planes para migrarlo a Rust junto con el resto del canal Solidity.
En Yul, el código se divide en objetos, que pueden contener código, datos y objetos anidados. Por lo tanto, no hay constantes ni funciones externas en Yul. Es necesario definir los despachadores de selectores de funciones para exponer las rutas de código al mundo exterior.
Con la excepción de las instrucciones de flujo de control y pila, la mayoría de las instrucciones se exponen como funciones en Yul. Las instrucciones pueden anidarse para acortar la longitud del código o asignarse a variables temporales y luego pasarse a otras instrucciones para su uso. Las ramas condicionales pueden usar un bloque "if", que se ejecuta si el valor no es cero, pero no hay un bloque "else", por lo que manejar múltiples rutas de código requiere el uso de un "interruptor" para manejar cualquier número de casos y una opción alternativa "predeterminada". Los bucles se pueden ejecutar utilizando un bucle "for"; aunque su sintaxis es diferente a la de otros lenguajes de alto nivel, proporciona la misma funcionalidad básica. Las funciones internas se pueden definir utilizando la palabra clave "función" y son similares a las definiciones de funciones en lenguajes de alto nivel.
La mayor parte de la funcionalidad en Yul se expone en Solidity mediante bloques de ensamblaje en línea. Esto permite a los desarrolladores romper abstracciones y escribir funciones personalizadas o utilizar Yul en funciones que no están disponibles en la sintaxis de alto nivel. Sin embargo, utilizar esta función requiere una comprensión profunda del comportamiento de Solidity con respecto a los datos de llamadas, la memoria y el almacenamiento.
También hay algunas funciones únicas. Las funciones "datasize", "dataoffset" y "datacopy" operan en objetos Yul a través de sus alias de cadena. Las funciones "setimmutable" y "loadimmutable" permiten configurar y cargar parámetros inmutables en el constructor, aunque su uso está restringido. La función "memoryguard" indica que solo se asigna un rango de memoria determinado, lo que permite al compilador usar memoria más allá del rango protegido para optimizaciones adicionales. Finalmente, "textalmente" permite el uso de instrucciones desconocidas para el compilador de Yul.
Aquí hay un contrato simple escrito en Yul:

Características de un buen EVM DSL
Un buen EVM DSL debe aprender de los pros y los contras de cada lenguaje enumerado aquí, y también debe incluir los conceptos básicos que se encuentran en casi todos los lenguajes modernos, como condicionales, coincidencia de patrones, bucles, funciones y más. El código debe ser explícito, con un mínimo de abstracciones implícitas agregadas en aras de la belleza o la legibilidad del código. En entornos de alto riesgo y en los que la corrección es crítica, cada línea de código debe ser interpretable sin ambigüedades. Además, un sistema de módulos bien definido debería ser el núcleo de cualquier gran lenguaje. Debe indicar claramente qué elementos están definidos, en qué ámbito y cuáles son accesibles. Cada elemento de un módulo debe ser privado de forma predeterminada y solo los elementos explícitamente públicos deben ser accesibles públicamente externamente.
En un entorno con recursos limitados como EVM, la eficiencia es importante. La eficiencia a menudo se logra proporcionando abstracciones de bajo costo, como la ejecución de código en tiempo de compilación a través de macros, un sistema de tipos enriquecido para crear bibliotecas reutilizables bien diseñadas y contenedores para interacciones comunes en cadena. Las macros generan código en tiempo de compilación, lo cual es útil para reducir el código repetitivo para operaciones comunes y, en casos como el de Huff, donde se puede usar para equilibrar el tamaño del código con la eficiencia del tiempo de ejecución. Un sistema de tipos enriquecido permite un código más expresivo, más comprobaciones en tiempo de compilación para detectar errores antes del tiempo de ejecución y, cuando se combina con los elementos intrínsecos del compilador con verificación de tipos, puede eliminar la necesidad de gran parte del ensamblado en línea. Los genéricos también permiten incluir valores que aceptan valores NULL (como código externo) en tipos de "opción" u operaciones propensas a errores (como llamadas externas) en tipos de "resultado". Estos dos tipos son ejemplos de cómo los escritores de bibliotecas obligan a los desarrolladores a manejar cada resultado definiendo rutas de código o transacciones que recuperan resultados fallidos. Sin embargo, tenga en cuenta que estas son abstracciones en tiempo de compilación que se resuelven en saltos condicionales simples en tiempo de ejecución. Obligar a los desarrolladores a manejar cada resultado en tiempo de compilación aumenta el tiempo de desarrollo inicial, pero el beneficio es que hay muchas menos sorpresas en tiempo de ejecución.
La flexibilidad también es importante para los desarrolladores, por lo que, si bien la ruta predeterminada para operaciones complejas debería ser la ruta segura y potencialmente menos eficiente, a veces es necesario utilizar rutas de código más eficientes o funcionalidades no compatibles. Para ello, el montaje en línea debe estar abierto a los desarrolladores, sin barreras de seguridad. El ensamblaje en línea de Solidity establece algunas barreras para la simplicidad y mejores pasos del optimizador, pero cuando los desarrolladores necesitan control total sobre el entorno de ejecución, se les deben otorgar estos derechos.
Algunas características potencialmente útiles incluyen la capacidad de manipular propiedades de funciones y otros elementos en tiempo de compilación. Por ejemplo, el atributo "en línea" puede copiar el cuerpo de una función simple en cada llamada, en lugar de crear más saltos para mayor eficiencia. El atributo "abi" le permite anular manualmente el ABI generado por una función externa determinada para adaptarse a idiomas con diferentes estilos de codificación. Además, se puede definir un programador de funciones opcional, lo que permite la personalización dentro del lenguaje de alto nivel para optimizaciones adicionales en las rutas de código que se espera que se utilicen con más frecuencia. Por ejemplo, verifique si el selector es "transferir" o "transferFrom" antes de ejecutar "nombre".
en conclusión
El diseño EVM DSL tiene un largo camino por recorrer. Cada idioma tiene sus propias decisiones de diseño únicas y espero ver cómo se desarrollan en el futuro. Como desarrolladores, lo mejor para nosotros es aprender tantos idiomas como sea posible. En primer lugar, aprender varios lenguajes y comprender sus diferencias y similitudes profundizará nuestra comprensión de la programación y la arquitectura subyacente de la máquina. En segundo lugar, el lenguaje tiene profundos efectos de red y fuertes propiedades de retención. No hay duda de que los grandes actores están creando sus propios lenguajes de programación, desde C#, Swift y Kotlin hasta Solidity, Sway y Cairo. Aprender a cambiar sin problemas entre estos idiomas proporciona una flexibilidad incomparable para una carrera de ingeniería de software. Finalmente, es importante entender que hay mucho trabajo detrás de cada idioma. Nadie es perfecto, pero innumerables personas talentosas se esfuerzan mucho para crear experiencias seguras y agradables para desarrolladores como nosotros.
