Além do Solidity, em quais outras linguagens EVM vale a pena prestar atenção?

Escrito por: jtriley.ethjtriley.eth

Compilado por: 0x11, Foresight News

A Máquina Virtual Ethereum (EVM) é uma máquina Turing de 256 bits, baseada em pilha e globalmente acessível. Devido à sua arquitetura significativamente diferente de outras máquinas virtuais e físicas, o EVM requer uma linguagem DSL específica de domínio (nota: uma linguagem específica de domínio refere-se a uma linguagem de computador que se concentra em um determinado domínio de aplicativo).

Neste artigo, veremos o que há de mais moderno em design de EVM DSL, abrangendo seis linguagens: Solidity, Vyper, Fe, Huff, Yul e ETK.

Versão do idioma

  • Solidez: 0.8.19

  • Vyper: 0,3,7

  • Fe: 0,21,0

  • Sopro: 0,3,1

  • ETC: 0.2.1

  • Yul: 0.8.19

A leitura deste artigo exige que você tenha um conhecimento básico de EVM, pilha e programação.

Visão geral da máquina virtual Ethereum

A EVM é uma máquina de Turing baseada em pilha de 256 bits. Porém, antes de nos aprofundarmos em seu compilador, alguns recursos funcionais devem ser introduzidos.

Como o EVM é "Turing completo", ele sofrerá com o "problema de parada". Resumindo, antes de um programa ser executado, não há como determinar se ele será encerrado no futuro. A forma como o EVM resolve este problema é medir unidades de cálculo através do “Gás”, que geralmente é proporcional aos recursos físicos necessários para executar as instruções. A quantidade de Gás por transação é limitada, e o iniciador da transação deve pagar ETH proporcional ao Gás consumido pela transação. Um impacto desta estratégia é que se existirem dois contratos inteligentes funcionalmente idênticos, aquele que consome menos gás será mais adotado. Isto resulta em protocolos que competem pela extrema eficiência do gás, com os engenheiros a esforçarem-se por minimizar o consumo de gás para tarefas específicas.

Além disso, quando um contrato é chamado, ele cria um contexto de execução. Neste contexto, o contrato possui uma pilha para operações e processamento, uma instância de memória linear para leitura e escrita, um armazenamento local persistente para leitura e escrita do contrato, e os dados anexados à chamada "calldata" podem ser lidos, mas não são gravados. .

Uma observação importante sobre a memória é que, embora não haja um “limite superior” definido para seu tamanho, ela ainda é limitada. O custo do gás para expandir a memória é dinâmico: quando um limite é atingido, o custo da expansão da memória aumenta quadraticamente, o que significa que o custo do gás é proporcional ao quadrado da alocação de memória adicional.

Os contratos também podem chamar outros contratos usando diversas instruções diferentes. A instrução "call" envia dados e ETH opcional para o contrato alvo e, em seguida, cria seu próprio contexto de execução até que a execução do contrato alvo seja interrompida. A diretiva "staticcall" é igual a "call", mas adiciona uma verificação que afirma que nenhuma parte do estado global foi atualizada antes da conclusão da chamada estática. Finalmente, a directiva “delegatecall” comporta-se como “call”, excepto que retém algumas informações ambientais do contexto anterior. Isso normalmente é usado para bibliotecas externas e contratos de proxy.

Por que o design da linguagem é importante

Linguagens específicas de domínio (DSLs) são necessárias ao interagir com arquiteturas atípicas. Embora existam cadeias de ferramentas de compilador como o LLVM, contar com elas para lidar com contratos inteligentes não é o ideal em situações em que a correção do programa e a eficiência computacional são críticas.

A correção do programa é importante porque os contratos inteligentes são imutáveis ​​por padrão e são uma escolha popular para aplicações financeiras, dadas as propriedades das máquinas virtuais (VMs) blockchain. Embora exista uma solução atualizável para o EVM, ela é, na melhor das hipóteses, um patch e, na pior, uma vulnerabilidade de execução de código arbitrário.

A eficiência computacional também é crítica, pois minimizar a computação traz vantagens económicas, mas não à custa da segurança.

Em suma, um EVM DSL deve equilibrar a correção do programa e a eficiência do gás, alcançando um ou outro fazendo diferentes compromissos sem sacrificar muita flexibilidade.

Visão geral do idioma

Para cada idioma, descreveremos seus principais recursos e opções de design e incluiremos um contrato inteligente com função de contagem simples. A popularidade verbal é determinada com base nos dados do Total Value Locked (TVL) no Defi Llama.

Solidez

Solidity é uma linguagem de alto nível cuja sintaxe é semelhante a C, Java e Javascript. É o idioma mais popular da TVL, com um TVL dez vezes maior que o próximo idioma mais popular. Para reutilização de código, utiliza um padrão orientado a objetos, onde contratos inteligentes são tratados como objetos de classe, aproveitando herança múltipla. O compilador é escrito em C++, com planos de migrar para Rust no futuro.

Os campos mutáveis ​​do contrato são armazenados em armazenamento persistente, a menos que seus valores sejam conhecidos em tempo de compilação (constantes) ou em tempo de implantação (imutáveis). Os métodos declarados em um contrato podem ser declarados puros, visíveis, pagáveis ​​ou não pagáveis ​​por padrão, mas com status modificável. Os métodos puros não leem dados do ambiente de execução e não podem ler ou gravar no armazenamento persistente, ou seja, dada a mesma entrada, os métodos puros sempre retornarão a mesma saída e não terão efeitos colaterais; Os métodos de visualização podem ler dados do armazenamento de persistência ou do ambiente de execução, mas não podem gravar no armazenamento de persistência, nem podem criar efeitos colaterais, como anexar logs de transações. Os métodos pagáveis ​​podem ler e gravar armazenamento persistente, ler dados do ambiente de execução, produzir efeitos colaterais e receber ETH anexado à chamada. O método não pagável é igual ao método pagável, mas possui uma verificação em tempo de execução para afirmar que não há ETH anexado no contexto de execução atual.

NOTA: Anexar ETH a uma transação é independente do pagamento de taxas de gás, o ETH anexado é recebido pelo contrato e você pode optar por aceitá-lo ou rejeitá-lo restaurando o contexto.

Quando declarados no escopo de um contrato, os métodos podem especificar quatro modificadores de visibilidade: privado, interno, público ou externo. Os métodos privados podem ser acessados ​​internamente através da instrução "jump" do contrato atual. Nenhum contrato herdado pode acessar diretamente métodos privados. Os métodos internos também podem ser acessados ​​internamente através da instrução "jump", mas os contratos herdados podem usar métodos internos diretamente. Os métodos públicos podem ser acessados ​​por contratos externos através da instrução "call", que cria um novo contexto de execução, e internamente através de saltos ao chamar o método diretamente. Os métodos públicos também podem ser acessados ​​a partir do mesmo contrato em um novo contexto de execução, acrescentando “this” à chamada do método. Os métodos externos só podem ser acessados ​​através da instrução "call", sejam eles de contratos diferentes ou dentro do mesmo contrato, "this" precisa ser adicionado antes da chamada do método.

Nota: A instrução "jump" opera o contador do programa e a instrução "call" cria um novo contexto de execução durante a execução do contrato alvo. Quando possível, use “pular” em vez de “ligar” para economizar gás.

O Solidity também oferece três maneiras de definir bibliotecas. A primeira é uma biblioteca externa, que é um contrato sem estado implantado separadamente na cadeia, vinculado dinamicamente quando o contrato é chamado e acessado por meio da instrução "delegatecall". Esta é a abordagem menos comum porque o suporte da ferramenta para bibliotecas externas é insuficiente, "delegatecall" é caro, deve carregar código adicional do armazenamento persistente e requer múltiplas transações para implantação. As bibliotecas internas são definidas da mesma forma que as bibliotecas externas, exceto que cada método deve ser definido como um método interno. Em tempo de compilação, a biblioteca interna é incorporada ao contrato final e, durante a fase de análise do código morto, os métodos não utilizados da biblioteca são removidos. A terceira forma é semelhante à biblioteca interna, mas em vez de definir estruturas de dados e funções dentro da biblioteca, elas são definidas no nível do arquivo e podem ser importadas diretamente e utilizadas no contrato final. A terceira abordagem proporciona melhor interatividade humano-computador usando estruturas de dados personalizadas, aplicando funções ao escopo global e aplicando operadores de alias a certas funções até certo ponto.

O compilador fornece duas etapas de otimização. O primeiro é o otimizador em nível de instrução, que realiza operações de otimização no bytecode final. A segunda é a recente adição do uso da linguagem Yul (mais sobre isso mais tarde) como uma representação intermediária (IR) durante o processo de compilação e, em seguida, realizar operações de otimização no código Yul gerado.

Para interagir com métodos públicos e externos em um contrato, o Solidity especifica um padrão Application Binary Interface (ABI) para interagir com seus contratos. Atualmente, o Solidity ABI é considerado o padrão de fato para DSLs EVM. Os padrões Ethereum ERC que especificam interfaces externas são implementados de acordo com a especificação ABI e guia de estilo do Solidity. Outras linguagens também seguem a especificação ABI do Solidity com pouquíssimos desvios.

O Solidity também fornece blocos Yul inline, permitindo acesso de baixo nível ao conjunto de instruções EVM. O bloco Yul contém um subconjunto de funcionalidades Yul; consulte a seção Yul para obter detalhes. Isso normalmente é usado para otimização de gás, aproveitando recursos não suportados pela sintaxe de nível superior e personalizando armazenamento, memória e dados de chamada.

Devido à popularidade do Solidity, as ferramentas de desenvolvedor são muito maduras e bem projetadas, e o Foundry se destaca nesse quesito.

A seguir está um contrato simples escrito em Solidity:

Víbora

Vyper é uma linguagem de alto nível com sintaxe semelhante ao Python. É quase um subconjunto do Python com algumas pequenas diferenças. É o segundo EVM DSL mais popular. O Vyper é otimizado para segurança, legibilidade, auditabilidade e eficiência de gás. Ele não usa padrões orientados a objetos, montagem embutida e não oferece suporte à reutilização de código. Seu compilador é escrito em Python.

As variáveis ​​armazenadas no armazenamento persistente são declaradas no nível do arquivo. Se o seu valor for conhecido em tempo de compilação, eles podem ser declarados como "constantes"; se seu valor for conhecido em tempo de implantação, eles podem ser declarados como "imutáveis" se estiverem marcados. Se for público, o contrato final irá expor; uma função somente leitura para a variável. Os valores de constantes e invariantes são acessados ​​​​internamente por meio de seus nomes, mas variáveis ​​​​mutáveis ​​​​no armazenamento persistente podem ser acessadas acrescentando “self” ao nome. Isto é útil para evitar conflitos de namespace entre variáveis ​​armazenadas, parâmetros de função e variáveis ​​locais.

Semelhante ao Solidity, o Vyper também usa atributos de função para representar a visibilidade e variabilidade das funções. As funções marcadas como “@external” podem ser acessadas a partir de contratos externos através da instrução “call”. As funções marcadas como “@internal” só podem ser acessadas dentro do mesmo contrato e devem ser prefixadas com “self”. As funções marcadas como "@pure" não podem ler dados do ambiente de execução ou armazenamento persistente, gravar no armazenamento persistente ou criar quaisquer efeitos colaterais. As funções marcadas como "@view" podem ler dados do ambiente de execução ou armazenamento persistente, mas não podem gravar no armazenamento persistente ou criar efeitos colaterais. As funções marcadas como “@payable” podem ler ou gravar em armazenamento persistente, criar efeitos colaterais e receber ETH. As funções que não declaram esse atributo de mutabilidade são padronizadas como não pagáveis, ou seja, são iguais às funções pagáveis, mas não podem receber ETH.

O compilador Vyper também opta por armazenar variáveis ​​locais na memória em vez de na pilha. Isso torna os contratos mais simples e eficientes e resolve o problema da "pilha muito profunda" comum em outras linguagens de alto nível. No entanto, isso também traz algumas compensações.

Além disso, como o layout da memória deve ser conhecido em tempo de compilação, a capacidade máxima dos tipos dinâmicos também deve ser conhecida em tempo de compilação, o que é uma limitação. Além disso, a alocação de grandes quantidades de memória pode levar ao consumo de gás não linear, conforme mencionado na seção de visão geral do EVM. No entanto, para muitos casos de uso, o custo do gás é insignificante.

Embora o Vyper não suporte montagem em linha, ele fornece mais funções integradas para garantir que quase todos os recursos do Solidity e Yul também estejam disponíveis no Vyper. Operações de bits de baixo nível, chamadas externas e operações de contrato de proxy podem ser acessadas por meio de funções integradas, e layouts de armazenamento personalizados podem ser implementados fornecendo arquivos de sobreposição em tempo de compilação.

O Vyper não possui um conjunto rico de ferramentas de desenvolvimento, mas possui ferramentas que são mais integradas e também podem ser conectadas às ferramentas de desenvolvimento do Solidity. Ferramentas notáveis ​​​​do Vyper incluem o interpretador Titanaboa, que possui muitas ferramentas integradas relacionadas ao EVM e Vyper para experimentação e desenvolvimento, e Dasy, um Lisp baseado em Vyper com execução de código em tempo de compilação.

Aqui está um contrato simples escrito em Vyper:

Fe é uma linguagem de alto nível como Rust que está atualmente em desenvolvimento ativo, com a maioria dos recursos ainda não disponíveis. Seu compilador é escrito principalmente em Rust, mas usa Yul como sua representação intermediária (IR), contando com o otimizador Yul escrito em C++. Espera-se que isso mude com a adição do Sonatina, um backend nativo do Rust. Fe usa módulos para compartilhamento de código, portanto não usa um padrão orientado a objetos, mas reutiliza código por meio de um sistema baseado em módulos, onde variáveis, tipos e funções são declarados dentro de módulos e podem ser importados de maneira semelhante ao Rust.

Variáveis ​​de armazenamento persistente são declaradas no nível do contrato e não são acessíveis publicamente sem funções getter definidas manualmente. As constantes podem ser declaradas no nível do arquivo ou módulo e acessíveis dentro do contrato. Variáveis ​​imutáveis ​​de tempo de implantação não são suportadas atualmente.

Os métodos podem ser declarados no nível do módulo ou dentro de um contrato e são puros e privados por padrão. Para tornar público um método de contrato, a palavra-chave "pub" deve ser adicionada antes da definição, o que o torna acessível externamente. Para ler de uma variável de armazenamento persistente, o primeiro parâmetro do método deve ser "self". Adicione "self" antes do nome da variável para fornecer ao método acesso somente leitura à variável de armazenamento local. Para ler e gravar no armazenamento persistente, o primeiro parâmetro deve ser "mut self". A palavra-chave “mut” indica que o armazenamento do contrato é mutável durante a execução do método. O acesso às variáveis ​​de ambiente é feito passando o parâmetro "Contexto" para o método, normalmente denominado "ctx".

Funções e tipos personalizados podem ser declarados no nível do módulo. Por padrão, os itens do módulo são privados e não podem ser acessados ​​a menos que a palavra-chave “pub” seja usada. No entanto, não deve ser confundido com a palavra-chave “pub” no nível do contrato. Os membros públicos de um módulo só podem ser acessados ​​dentro do contrato final ou de outros módulos.

Atualmente, o Fe não oferece suporte a assembly embutido; em vez disso, as instruções são agrupadas por intrínsecos do compilador ou funções especiais que resolvem as instruções em tempo de compilação.

Fe segue a sintaxe e o sistema de tipos do Rust, suportando aliases de tipo, enums com subtipos, características e genéricos. O suporte para isso é atualmente limitado, mas está sendo trabalhado. As características podem ser definidas e implementadas para diferentes tipos, mas genéricos e restrições de características não são suportados. As enumerações suportam subtipos e métodos podem ser implementados nelas, mas não podem ser codificados em funções externas. Embora o sistema de tipos do Fe ainda seja um trabalho em andamento, ele mostra muito potencial para escrever códigos mais seguros e verificados em tempo de compilação para desenvolvedores.

Aqui está um contrato simples escrito em Fe:

Bufar

Huff é uma linguagem assembly com controle manual de pilha e abstração mínima do conjunto de instruções EVM. Através da diretiva "#include", quaisquer arquivos Huff incluídos podem ser analisados ​​durante a compilação para obter a reutilização do código. Originalmente escrito pela equipe Aztec para algoritmos de curva elíptica extremamente otimizados, o compilador foi posteriormente reescrito em TypeScript e depois em Rust.

As constantes devem ser definidas em tempo de compilação, imutáveis ​​não são suportados atualmente e variáveis ​​de armazenamento persistentes não são definidas explicitamente na linguagem. Como as variáveis ​​de armazenamento nomeadas são uma abstração de alto nível, a gravação no armazenamento persistente no Huff é feita por meio dos opcodes "sstore" para gravação e "sload" para leitura. Layouts de armazenamento personalizados podem ser definidos pelo usuário ou você pode seguir a convenção de começar do zero e incrementar cada variável usando o "FREE_STORAGE_POINTER" integrado do compilador. Tornar uma variável armazenada acessível externamente requer a definição manual de um caminho de código que possa ler e retornar a variável ao chamador.

Funções externas também são abstrações introduzidas por linguagens de alto nível, portanto não existe conceito de funções externas em Huff. No entanto, a maioria dos projetos segue em graus variados as especificações ABI de outras linguagens de alto nível, mais comumente Solidity. Um padrão comum é definir um "despachante" que carrega os dados brutos da chamada e os utiliza para verificar se há seletores de função correspondentes. Se corresponder, seu código subsequente será executado. Como os escalonadores são definidos pelo usuário, eles podem seguir diferentes padrões de escalonamento. O Solidity classifica os seletores em seus escalonadores em ordem alfabética por nome, o Vyper classifica numericamente e executa uma pesquisa binária em tempo de execução, e a maioria dos escalonadores Huff classifica por frequência de uso de função esperada, raramente usando tabelas de salto. Atualmente, as tabelas de salto não são suportadas nativamente no EVM, portanto, instruções de introspecção como "codecopy" precisam ser usadas para implementá-las.

As funções internas são definidas usando a diretiva #definefn", que pode aceitar parâmetros de modelo para maior flexibilidade e especificar a profundidade de pilha esperada no início e no final da função. Como estas funções são internas, não podem ser acessadas externamente. O acesso interno requer o uso da instrução "salto".

Outros fluxos de controle, como instruções condicionais e instruções de loop, podem usar definições de destino de salto. O alvo do salto é definido por um identificador seguido por dois pontos. Os saltos para esses alvos podem ser feitos colocando o identificador na pilha e executando uma instrução de salto. Isso é resolvido para um deslocamento de bytecode em tempo de compilação.

As macros são definidas por #definemacro" e são iguais às funções internas. A principal diferença é que a macro não gera uma instrução de "salto" em tempo de compilação, mas copia o corpo da macro diretamente em cada chamada do arquivo.

Esse design compensa a redução de saltos arbitrários em relação ao custo do gás de tempo de execução em detrimento do aumento do tamanho do código quando chamado várias vezes. A macro “MAIN” é considerada o ponto de entrada do contrato, e a primeira instrução em seu corpo se tornará a primeira instrução no bytecode de tempo de execução.

Outros recursos integrados ao compilador incluem geração de hash de eventos para registro, seletores de função para agendamento, seletores de erros para tratamento de erros e verificadores de tamanho de código para funções internas e macros.

Nota: Comentários de pilha como "//[count]" não são obrigatórios, eles são usados ​​apenas para indicar o estado da pilha no final da execução da linha.

Aqui está um contrato simples escrito em Huff:

ETC

O EVM Toolkit (ETK) é uma linguagem assembly com gerenciamento manual de pilha e abstrações mínimas. O código pode ser reutilizado através das diretivas "%include" e "%import", e o compilador é escrito em Rust.

Uma diferença significativa entre Huff e ETK é que Huff adiciona uma ligeira abstração ao initcode, também conhecido como código construtor, que pode ser substituído pela definição de uma macro especial "CONSTRUCTOR". No ETK estes não são abstraídos, o código de inicialização e o código de tempo de execução devem ser definidos juntos.

Semelhante ao Huff, o ETK lê e grava armazenamento persistente por meio das instruções “sload” e “sstore”. No entanto, não existe uma palavra-chave constante ou imutável, mas as constantes podem ser simuladas usando uma das duas macros no ETK, a macro de expressão. As macros de expressão não resolvem instruções, mas geram valores numéricos que podem ser usados ​​em outras instruções. Por exemplo, pode não gerar o comando “push” inteiramente, mas pode gerar um número para incluir no comando “push”.

Conforme mencionado anteriormente, as funções externas são um conceito de linguagem de alto nível, portanto, expor o caminho do código externamente requer a criação de um escalonador de seletor de função.

As funções internas não são definidas explicitamente como em outras linguagens. Em vez disso, aliases definidos pelo usuário podem ser fornecidos aos alvos de salto e saltam para eles por seus nomes. Isto também permite outros fluxos de controle, como loops e instruções condicionais.

ETK oferece suporte a dois tipos de macros. A primeira é uma macro de expressão que aceita qualquer número de argumentos e retorna um valor numérico que pode ser usado em outras instruções. As macros de expressão não geram instruções, mas sim valores ou constantes imediatas. No entanto, as macros diretivas aceitam qualquer número de argumentos e geram qualquer número de diretivas em tempo de compilação. As macros de instrução no ETK são semelhantes às macros Huff.

A seguir está um contrato simples escrito em ETK:

Yul

Yul é uma linguagem assembly com fluxo de controle de alto nível e um grande número de abstrações. Faz parte do conjunto de ferramentas do Solidity e pode ser opcionalmente usado em passes de construção do Solidity. Yul não oferece suporte à reutilização de código porque se destina a ser um alvo de compilação em vez de uma linguagem independente. Seu compilador é escrito em C++ e há planos de migrá-lo para Rust junto com o restante do canal Solidity.

No Yul, o código é dividido em objetos, que podem conter código, dados e objetos aninhados. Portanto, não existem constantes ou funções externas em Yul. Os despachantes do seletor de função precisam ser definidos para expor os caminhos do código ao mundo externo.

Com exceção das instruções de pilha e fluxo de controle, a maioria das instruções são expostas como funções em Yul. As instruções podem ser aninhadas para reduzir o comprimento do código ou atribuídas a variáveis ​​temporárias e depois passadas para outras instruções para uso. As ramificações condicionais podem usar um bloco "if", que é executado se o valor for diferente de zero, mas não há bloco "else", portanto, lidar com vários caminhos de código requer o uso de um "switch" para lidar com qualquer número de casos e uma opção de fallback "padrão". Os loops podem ser executados usando um loop "for", embora sua sintaxe seja diferente de outras linguagens de alto nível, ela fornece a mesma funcionalidade básica; Funções internas podem ser definidas usando a palavra-chave "function" e são semelhantes às definições de funções em linguagens de alto nível.

A maior parte da funcionalidade do Yul é exposta no Solidity usando blocos de montagem embutidos. Isso permite que os desenvolvedores quebrem abstrações e escrevam funcionalidades personalizadas ou usem Yul em funcionalidades não disponíveis na sintaxe de alto nível. No entanto, o uso desse recurso requer um conhecimento profundo do comportamento do Solidity em relação aos dados de chamada, memória e armazenamento.

Existem também algumas funções exclusivas. As funções "datasize", "dataoffset" e "datacopy" operam em objetos Yul por meio de seus aliases de string. As funções "setimmutable" e "loadimmutable" permitem configurar e carregar parâmetros imutáveis ​​no construtor, embora seu uso seja restrito. A função "memoryguard" indica que apenas um determinado intervalo de memória é alocado, permitindo ao compilador usar memória além do intervalo protegido para otimizações adicionais. Finalmente, "literalmente" permite o uso de instruções desconhecidas pelo compilador Yul.

Aqui está um contrato simples escrito em Yul:

Características de um bom EVM DSL

Uma boa EVM DSL deve aprender com os prós e os contras de cada linguagem listada aqui e também deve incluir o básico encontrado em quase todas as linguagens modernas, como condicionais, correspondência de padrões, loops, funções e muito mais. O código deve ser explícito, com o mínimo de abstrações implícitas adicionadas para fins de beleza ou legibilidade do código. Em ambientes de alto risco e críticos para a correção, cada linha de código deve ser interpretável de forma inequívoca. Além disso, um sistema de módulos bem definido deve estar no centro de qualquer grande linguagem. Deve indicar claramente quais itens estão definidos em qual escopo e quais são acessíveis. Cada item em um módulo deve ser privado por padrão, e apenas itens explicitamente públicos devem ser publicamente acessíveis externamente.

Num ambiente com recursos limitados como o EVM, a eficiência é importante. A eficiência geralmente é alcançada fornecendo abstrações de baixo custo, como execução de código em tempo de compilação por meio de macros, um sistema de tipo rico para criar bibliotecas reutilizáveis ​​bem projetadas e wrappers para interações comuns na cadeia. As macros geram código em tempo de compilação, o que é útil para reduzir o código clichê para operações comuns e em casos como o de Huff, onde pode ser usado para equilibrar o tamanho do código versus a eficiência do tempo de execução. Um sistema de tipo rico permite código mais expressivo, mais verificações em tempo de compilação para detectar erros antes do tempo de execução e, quando combinado com intrínsecos do compilador com verificação de tipo, pode eliminar a necessidade de grande parte do assembly embutido. Os genéricos também permitem que valores anuláveis ​​(como código externo) sejam agrupados em tipos de "opção" ou que operações propensas a erros (como chamadas externas) sejam agrupadas em tipos de "resultado". Esses dois tipos são exemplos de como os criadores de bibliotecas forçam os desenvolvedores a lidar com cada resultado definindo caminhos de código ou transações que recuperam resultados com falha. No entanto, lembre-se de que essas são abstrações em tempo de compilação que resultam em simples saltos condicionais em tempo de execução. Forçar os desenvolvedores a lidar com todos os resultados em tempo de compilação aumenta o tempo de desenvolvimento inicial, mas a vantagem é que há muito menos surpresas em tempo de execução.

A flexibilidade também é importante para os desenvolvedores, portanto, embora o padrão para operações complexas deva ser a rota segura e potencialmente menos eficiente, às vezes é necessário usar caminhos de código mais eficientes ou funcionalidades não suportadas. Para fazer isso, a montagem inline deve ser aberta aos desenvolvedores, sem proteções. A montagem inline do Solidity estabelece algumas barreiras para simplicidade e melhores passagens do otimizador, mas quando os desenvolvedores precisam de controle total sobre o ambiente de execução, eles devem receber esses direitos.

Alguns recursos potencialmente úteis incluem a capacidade de manipular propriedades de funções e outros itens em tempo de compilação. Por exemplo, o atributo "inline" pode copiar o corpo de uma função simples em cada chamada, em vez de criar mais saltos para maior eficiência. O atributo “abi” permite substituir manualmente a ABI gerada por uma determinada função externa para se adaptar a linguagens com diferentes estilos de codificação. Além disso, um agendador de função opcional pode ser definido, permitindo a personalização na linguagem de alto nível para otimizações adicionais em caminhos de código que serão usados ​​com mais frequência. Por exemplo, verifique se o seletor é “transfer” ou “transferFrom” antes de executar “name”.

para concluir

O design EVM DSL ainda tem um longo caminho a percorrer. Cada linguagem tem suas próprias decisões de design e estou ansioso para ver como elas se desenvolverão no futuro. Como desenvolvedores, é do nosso interesse aprender o máximo de idiomas possível. Primeiro, aprender múltiplas linguagens e compreender suas diferenças e semelhanças aprofundará nossa compreensão da programação e da arquitetura de máquina subjacente. Em segundo lugar, a linguagem tem efeitos de rede profundos e fortes propriedades de retenção. Não há dúvida de que os grandes players estão construindo suas próprias linguagens de programação, desde C#, Swift e Kotlin até Solidity, Sway e Cairo. Aprender a alternar perfeitamente entre essas linguagens oferece flexibilidade incomparável para uma carreira em engenharia de software. Por fim, é importante entender que há muito trabalho por trás de cada idioma. Ninguém é perfeito, mas inúmeras pessoas talentosas se esforçam muito para criar experiências seguras e agradáveis ​​para desenvolvedores como nós.