Comparando o mesmo projeto em Rust, Haskell, C ++, Python, Scala e OCaml

No último semestre da universidade, escolhi o curso de compilação CS444 . Lá, cada grupo de 1 a 3 pessoas precisava escrever um compilador a partir de um subconjunto substancial de Java no x86. Idioma para escolher um grupo. Essa foi uma rara oportunidade para comparar implementações de grandes programas da mesma funcionalidade, escritas por programadores muito competentes em diferentes idiomas, e comparar a diferença de design e escolha de idioma. Essa comparação deu origem a muitos pensamentos interessantes. Essa comparação controlada de idiomas é raramente vista. Não é perfeito, mas muito melhor do que a maioria das histórias subjetivas nas quais as opiniões das pessoas sobre linguagens de programação se baseiam.

Criamos nosso compilador Rust, e primeiro eu o comparei com o projeto da equipe Haskell. Eu esperava que o programa deles fosse muito mais curto, mas acabou sendo do mesmo tamanho ou maior. O mesmo vale para o OCaml. Depois, comparei-o com o compilador C ++, e era de se esperar que o compilador fosse cerca de 30% maior, principalmente devido a cabeçalhos, falta de tipos de soma e correspondência de padrões. A comparação a seguir foi com minha amiga, que fez o compilador sozinha em Python e usou menos da metade do código comparado a nós, devido ao poder da metaprogramação e dos tipos dinâmicos. Outro amigo tinha um programa Scala menor. O que mais me surpreendeu foi a comparação com outra equipe que também usava o Rust, mas eles tiveram três vezes mais código devido a diferentes decisões de design. No final, a maior diferença na quantidade de código estava no mesmo idioma!

Vou explicar por que considero isso uma boa comparação, fornecer algumas informações sobre cada projeto e explicar alguns motivos para diferenças no tamanho do compilador. Também tirarei conclusões de cada comparação. Sinta-se à vontade para usar esses links para acessar a seção de interesse:

Conteúdo


  • Por que acho isso significativo
  • Ferrugem (base de comparação)
  • Haskell : 1,0-1,6 tamanhos, dependendo de como você conta, por razões interessantes
  • C ++ : 1,4 tamanhos por razões óbvias
  • Python : tamanho 0.5 devido a metaprogramação sofisticada!
  • Ferrugem (outro grupo) : três vezes o tamanho devido a um design diferente!
  • Scala : 0,7 tamanhos
  • OCaml : tamanho 1.0-1.6, dependendo de como você conta, semelhante ao Haskell

Por que acho isso significativo


Antes de você dizer que a quantidade de código (comparei as seqüências de caracteres e os bytes) é uma métrica terrível, quero observar que, nesse caso, ele pode fornecer um bom entendimento. Pelo menos, este é o exemplo mais bem controlado em que equipes diferentes escrevem o mesmo grande programa que ouvi ou li.

  • Ninguém (inclusive eu) sabia que eu mediria esse parâmetro; portanto, ninguém tentou reproduzir métricas; todos tentaram terminar o projeto de maneira rápida e correta.
  • Todos (com exceção do projeto Python, que discutirei mais adiante) implementaram o programa com o único objetivo de passar no mesmo conjunto de testes automatizados no mesmo período de tempo, para que os resultados não possam ser muito distorcidos por grupos que resolvem problemas diferentes.
  • O projeto foi concluído em alguns meses, com a equipe, e deveria expandir gradualmente e passar em testes conhecidos e desconhecidos. Isso significa que foi útil escrever um código limpo e claro.
  • Além de passar nos testes do curso, o código não será usado para mais nada, ninguém irá lê-lo e, sendo um compilador para um subconjunto limitado de Java no assembler de texto, não será útil.
  • Nenhuma biblioteca diferente da biblioteca padrão é permitida e nenhum auxiliar para análise, mesmo que eles estejam na biblioteca padrão. Isso significa que a comparação não pode ser distorcida pelas poderosas bibliotecas do compilador que apenas alguns comandos possuem.
  • Não foram apenas testes públicos, mas também secretos. Eles começaram uma vez após a entrega final. Isso significava que havia um incentivo para escrever seu próprio código de teste e garantir que o compilador fosse confiável, correto e lidasse com situações complexas de borda.
  • Embora todos os participantes sejam estudantes, considero-os programadores bastante competentes. Cada um deles fez estágios por pelo menos dois anos, principalmente em empresas de alta tecnologia, às vezes até trabalhando em compiladores. Quase todos estão programados por 7 a 13 anos e são entusiastas que lêem muito na Internet fora de seus cursos.
  • O código gerado não foi levado em consideração, mas os arquivos gramaticais e o código que gerou o outro código foram levados em consideração.

Portanto, acho que a quantidade de código fornece uma compreensão decente de quanto esforço será necessário para apoiar cada projeto, se for de longo prazo. Penso que não há muita diferença entre os projetos que também permite refutar algumas declarações extraordinárias que li, por exemplo, que o compilador Haskell terá mais da metade do tamanho do C ++ devido à linguagem.

Ferrugem (base de comparação)


Eu e um de meus camaradas escrevemos mais de 10 mil linhas em Rust antes e o terceiro colega escreveu, talvez, 500 linhas em algumas hackathons. Nosso compilador saiu em 6806 linhas de wc -l , 5900 linhas de origem (sem espaços e comentários) e 220 KB wc -c .

Descobri que em outros projetos essas proporções são mais ou menos respeitadas, com algumas exceções, que observarei. No restante do artigo, quando me refiro a strings ou somas, quero dizer wc -l , mas isso não importa (a menos que note a diferença), e você pode converter com um coeficiente.

Escrevi outro artigo descrevendo nosso design , que passou em todos os testes públicos e secretos. Ele também contém alguns recursos adicionais que criamos para nos divertir, não para passar nos testes, que provavelmente adicionaram cerca de 400 linhas. Ele também possui cerca de 500 linhas de nossos testes de unidade.

Haskell


A equipe do Haskell incluiu dois de meus amigos que escreveram talvez algumas linhas de Haskell cada um, além de ler muito conteúdo on-line sobre o Haskell e outras linguagens funcionais semelhantes, como OCaml e Lean. Eles tinham outro colega de equipe que eu não conhecia muito bem, mas parece que um programador forte usou Haskell antes.

Seu compilador totalizou 9.750 linhas de wc -l , 357 KB e 7777 linhas de código (SLOC). Essa equipe também possui as únicas diferenças significativas entre essas proporções: o compilador é 1,4 vezes maior que o nosso nas linhas, 1,3 vezes no SLOC e 1,6 vezes em bytes. Eles não implementaram nenhuma função adicional, passaram 100% dos testes públicos e secretos.

É importante observar que a inclusão de testes afetou principalmente essa equipe. Como eles abordaram cuidadosamente a exatidão do código, incluíram 1.600 linhas de testes. Eles capturaram várias situações limítrofes que nossa equipe não detectou, mas esses casos simplesmente não foram verificados pelos testes do curso. Portanto, sem testes nos dois lados (6,3 mil linhas versus 8,1 mil linhas), o compilador é apenas 30% a mais que o nosso.

Aqui, eu tendem a bytes como uma medida mais razoável da comparação de volumes, porque em um projeto Haskell, em média, existem linhas mais longas, pois ele não possui um grande número de linhas de um parêntese de fechamento e o rustfmt não rustfmt cadeias de funções de linha única em várias linhas.

Depois de vasculhar um de meus colegas de equipe, chegamos à seguinte explicação para essa diferença:

  • Utilizamos um analisador lexical manuscrito e um método de descida recursiva, e eles usaram um gerador NFA e DFA e um analisador LR e, em seguida, um passe para converter a árvore de análise em AST ( árvore de sintaxe abstrata , representação mais conveniente do código). Isso deu a eles significativamente mais código: 2677 linhas em comparação com o 1705, cerca de 1000 linhas a mais.
  • Eles usaram o fantástico AST genérico, que passou para vários parâmetros de tipo à medida que mais informações eram adicionadas em cada passagem. Essa e outras funções auxiliares para reescrita provavelmente explicam por que o código AST é cerca de 500 linhas mais longo que a nossa implementação, onde coletamos literais de estrutura e alteramos os campos Option<_> para adicionar informações à medida que avançamos.
  • Eles ainda têm cerca de 400 linhas de código durante a geração, as quais estão associadas principalmente à maior abstração necessária para gerar e combinar o código de uma maneira puramente funcional, onde simplesmente usamos linhas de mutação e escrita.

Essas diferenças mais testes explicam todas as diferenças de volume. De fato, nossos arquivos para constantes dobráveis ​​e resolução de contexto são muito próximos. Mas, ainda assim, há alguma diferença nos bytes devido às linhas mais longas: provavelmente porque é necessário mais código para reescrever a árvore inteira em cada passagem.

Como resultado, deixando de lado as decisões de design, Rust e Haskell são igualmente expressivas, talvez com uma ligeira vantagem, Rust, devido à capacidade de usar facilmente a mutação quando for conveniente. Também foi interessante saber que minha escolha do método de descida recursiva e do analisador lexical manuscrito valeu a pena: era um risco que contradiz as recomendações e instruções do professor, mas decidi que era mais fácil e correto.

Os fãs de Haskell argumentarão que essa equipe provavelmente não tirou o máximo proveito dos recursos de Haskell e, se eles conhecessem melhor o idioma, poderiam fazer um projeto com menos código. Concordo que alguém como Edward Kmett pode escrever o mesmo compilador em uma quantidade muito menor. De fato, a equipe de meu amigo não usou muitas abstrações super avançadas avançadas e bibliotecas combinadoras sofisticadas, como lentes . No entanto, tudo isso afeta a legibilidade do código. Todas as pessoas da equipe são programadores experientes, eles sabiam que Haskell era capaz de coisas muito bizarras, mas decidiram não usá-las porque decidiram que entendê-las levaria mais tempo do que economizariam e tornariam o código mais difícil para os outros entenderem. Isso parece um compromisso real para mim, e a afirmação de que Haskell é magicamente adequado para compiladores entra em algo como "Haskell requer qualificações extremamente altas para escrever compiladores se você não se importa com o suporte de código para pessoas que também não são muito adeptas de Haskell".

Também é interessante notar que, no início de cada projeto, o professor diz que os alunos podem usar qualquer idioma que funcione em um servidor universitário, mas alerta que as equipes de Haskell são diferentes das demais: elas têm a maior dispersão nas notas. Muitas pessoas superestimam suas habilidades e as equipes Haskell têm as notas mais ruins, embora outras se saiam bem como meus amigos.

C ++


Então eu conversei com meu amigo da equipe C ++. Eu conhecia apenas uma pessoa nessa equipe, mas o C ++ é usado em vários cursos da nossa universidade, portanto, provavelmente todos na equipe tinham experiência em C ++.

Seu projeto consistiu em 8733 linhas e 280 KB, não incluindo o código de teste, mas incluindo cerca de 500 linhas de funções adicionais. O que o torna 1,4 vezes maior que o nosso código sem testes, que também possui cerca de 500 linhas de funções adicionais. Eles passaram em 100% dos testes públicos, mas apenas 90% dos testes secretos. Presumivelmente, porque eles não implementaram os arrays vtables sofisticados exigidos pela especificação, que ocupam talvez de 50 a 100 linhas de código.

Não me aprofundei muito nessas diferenças de tamanho. Eu acho que isso se deve principalmente a:

  • Eles usam o analisador LR e o reescritor de árvores em vez do método de descida recursiva.
  • A falta de tipos de soma e comparações de padrões em C ++, amplamente utilizados e muito úteis.
  • A necessidade de duplicar todas as assinaturas nos arquivos de cabeçalho, o que não é o caso no Rust.

Também comparamos o tempo de compilação. No meu laptop, a compilação de depuração limpa de nosso compilador leva 9,7 s, a versão limpa 12,5 se a depuração incremental compila 3,5 s. Meu amigo não tinha horários disponíveis para sua compilação C ++ (usando make paralelo), mas ele disse que os números são semelhantes, com a ressalva de que eles colocam implementações de muitas funções pequenas nos arquivos de cabeçalho para reduzir a duplicação de assinaturas ao custo de um tempo mais longo (ou seja, portanto, não consigo medir a sobrecarga da linha de rede nos arquivos de cabeçalho).

Python


Meu amigo, um bom programador, decidiu fazer o projeto sozinho em Python. Ela também implementou recursos mais avançados (por diversão) do que qualquer outra equipe, incluindo uma visão SSA intermediária com alocação de registros e outras otimizações. Por outro lado, como funcionava sozinho e implementava muitas funções adicionais, prestava menos atenção à qualidade do código, por exemplo, lançando exceções indiferenciadas para todos os erros (dependendo de backtraces para depuração) em vez de implementar tipos de erro e mensagens correspondentes, como nós

Seu compilador consistia em 4581 linhas e passou em todos os testes públicos e secretos. Ela também implementou funções mais avançadas do que qualquer outro comando, mas é difícil determinar quanto código extra foi necessário, porque muitas das funções adicionais eram versões mais poderosas de coisas simples que todos precisavam implementar, como dobrar constantes e gerar código. Funções adicionais são provavelmente de 1000 a 2000 linhas, pelo menos, por isso tenho certeza de que o código dela é pelo menos duas vezes mais expressivo que o nosso.

Uma grande parte dessa diferença é provavelmente a digitação dinâmica. Somente em nosso ast.rs 500 linhas de definições de tipos e muitos outros tipos definidos em outras partes do compilador. Também estamos sempre limitados ao próprio sistema de tipos. Por exemplo, precisamos de uma infraestrutura para adicionar ergonomicamente novas informações ao AST à medida que passamos e as acessamos mais tarde. Enquanto estiver no Python, você pode apenas definir novos campos nos nós AST.

A metaprogramação poderosa também explica parte da diferença. Por exemplo, embora ela tenha usado um analisador LR em vez de um método de descida recursiva, no meu caso, acho que foi necessário menos código porque, em vez de passar por uma reescrita em árvore, sua gramática LR incluía partes do código Python para criar o AST, que o gerador poderia transformar em funções Python usando eval . Parte do motivo pelo qual não usamos o analisador LR é que a construção de um AST sem reescrever a árvore exigirá muita cerimônia (criação de arquivos Rust ou macros de procedimento) para associar a gramática a fragmentos do código Rust.

Outro exemplo do poder da metaprogramação e da digitação dinâmica é o arquivo visit.rs 400 linhas, que é basicamente um código repetitivo que implementa um visitante em várias estruturas AST. No Python, isso pode ser uma função curta de cerca de 10 linhas que introspecta recursivamente os campos de um nó AST e os visita (usando o atributo __dict__ ).

Como um fã do Rust e das linguagens de tipo estaticamente em geral, estou inclinado a observar que o sistema de tipos é muito útil para evitar erros e desempenho. A metaprogramação incomum também pode dificultar a compreensão de como o código funciona. No entanto, essa comparação me surpreendeu pelo fato de eu não esperar que a diferença na quantidade de código fosse tão grande. Se a diferença como um todo está realmente perto de ter que escrever o dobro do código, ainda acho que Rust é um compromisso adequado, mas ainda assim metade do código é um argumento e, no futuro, tendem a fazer algo em Ruby / Python se você só precisa criar rapidamente algo sozinho e jogá-lo fora.

Ferrugem (outro grupo)


A comparação mais interessante para mim foi com meu amigo, que também estava fazendo um projeto em Rust com um colega de equipe (que eu não conhecia). Meu amigo teve uma boa experiência de ferrugem. Ele contribuiu para o desenvolvimento do compilador Rust e leu muito. Não sei nada sobre o camarada dele.

Seu projeto consistiu em 17.211 linhas brutas, 15k linhas de origem e 637 KB, sem incluir o código de teste e o código gerado. Não possuía funções adicionais e passou em apenas 4 dos 10 testes secretos e 90% dos testes públicos para geração de código, porque eles não tinham tempo suficiente antes do prazo para implementar partes mais bizarras da especificação. O programa deles é três vezes maior que o nosso, escrito no mesmo idioma e com menos funcionalidade!

Esse resultado foi realmente incrível para mim e ofuscou todas as diferenças entre os idiomas que estudei até agora. Portanto, comparamos as listas de tamanhos de arquivo wc -l e também verificamos como cada um de nós implementou algumas coisas específicas que resultaram em diferentes tamanhos de código.

Parece que tudo se resume à adoção consistente de várias decisões de design. Por exemplo, o front-end (análise lexical, análise, construção de AST) leva 7597 linhas contra o nosso 2164. Eles usaram o analisador lexical do DFA e o analisador LALR (1), mas outros grupos fizeram coisas semelhantes sem tanto código. Observando o arquivo weeder, notei várias decisões de design diferentes das nossas:

  • Eles decidiram usar uma árvore de análise totalmente digitada em vez de uma árvore de análise padrão, uniforme e baseada em string. Provavelmente, isso exigiu muito mais definições de tipo e código de conversão adicional no estágio de análise ou um analisador mais complexo.
  • Eles usaram implementações tryfrom para converter entre analisar tipos de árvores e tipos AST para validá-los. Isso leva a muitos blocos de impl 10 a 20 linhas. Para isso, usamos funções que retornam tipos de Result , que geram menos linhas e também nos libertam um pouco da estrutura de tipos, simplificando a parametrização e a reutilização. Algumas das coisas que, para nós, eram ramificações de linha match , tinham blocos de impl 10 linhas.
  • Nossos tipos são estruturados para reduzir copiar e colar. Por exemplo, eles usaram campos separados is_abstract , is_native e is_static , nos quais o código de verificação de restrição teve que ser copiado duas vezes: uma para métodos com tipo de vazio e outra para métodos com tipo de retorno, com pequenas modificações. Embora nosso void fosse apenas um tipo especial, criamos uma taxonomia de modificadores com mode e visibility que aplicavam restrições no nível de tipo e erros de restrição eram gerados por padrão para o operador de correspondência, que convertia os conjuntos de modificadores em mode e visibility .

Eu não olhei para o código das passagens da análise do compilador, mas elas também são ótimas. Conversei com meu amigo e parece que eles não implementaram nada parecido com a infraestrutura dos visitantes, como a nossa. Eu acho que, junto com outras diferenças menores de design, explica a diferença de tamanho desta parte. O visitante permite que nossas passagens de análise se concentrem apenas nas partes do AST necessárias, em vez de corresponderem ao padrão em toda a estrutura do AST. Isso economiza muito código.

A parte deles para a geração de código consiste em 3594 linhas, e a nossa - 1560. Olhei para o código e parece que quase toda a diferença é que eles escolheram uma estrutura de dados intermediária para instruções do assembler, onde usamos apenas a formatação de string para saída direta do assembler . Eles tiveram que definir tipos e funções de saída para todas as instruções e tipos de operandos usados. Isso também significava que as instruções de montagem da construção exigiam mais código. Onde tínhamos um operador de formato com instruções curtas, como mov ecx, [edx] , eles precisavam de um operador rustfmt gigante, dividido em 6 linhas, que construíram uma instrução com rustfmt tipos aninhados intermediários para operandos que incluem até 6 níveis de colchetes aninhados. Também poderíamos emitir blocos de instruções relacionadas, como um preâmbulo de função, em uma instrução de formato único, onde eles precisavam especificar a construção completa para cada instrução.

Nossa equipe estava pensando em usar uma abstração como a deles. Era mais fácil ser capaz de produzir uma montagem de texto ou emitir diretamente o código da máquina, mas isso não era um requisito do curso. O mesmo poderia ser feito com menos código e melhor desempenho usando a X86Writer X86Writer com métodos como push(reg: Register) . Também levamos em conta que isso poderia simplificar a depuração e o teste, mas percebemos que a visualização do assembler de texto gerado é realmente mais fácil de ler e testar usando o snapshot testing se você inserir comentários livremente. Mas nós (aparentemente corretamente) previmos que seria necessário muito código adicional, e não havia nenhum benefício real, dadas as nossas reais necessidades, portanto não nos preocupamos.

É bom comparar isso com a representação intermediária que a equipe de C ++ usou como uma função extra, o que levou apenas 500 linhas extras. Eles usaram uma estrutura muito simples (para definições simples de tipo e código de construção) que usava operações próximas ao que o Java exigia. Isso significava que sua representação intermediária era muito menor (e, portanto, exigia menos código de construção) do que o assembler resultante, uma vez que muitas operações de linguagem, como chamadas e conversões, foram expandidas em muitas instruções do assembler. Eles também dizem que isso realmente ajudou na depuração, pois eliminou muito lixo e melhorou a legibilidade. Uma apresentação de nível superior também permitiu que algumas otimizações simples fossem feitas em sua representação intermediária. A equipe do C ++ criou um design muito bom, que os fez muito mais bem com muito menos código.

Em geral, parece que o motivo comum para a tríplice diferença de volume se deve à adoção consistente de várias decisões de design, grandes e pequenas, na direção de mais código. Eles implementaram uma série de abstrações que nós não fizemos - adicionaram mais código e pularam algumas de nossas abstrações, o que reduz a quantidade de código.

Esse resultado realmente me surpreendeu. Eu sabia que as decisões de design são importantes, mas não teria adivinhado antecipadamente que elas levariam a diferenças desse tamanho, uma vez que só examinava pessoas que considero fortes programadores competentes. De todos os resultados da comparação, este é o mais significativo para mim.Provavelmente me ajudou a ler muito sobre como escrever compiladores antes de fazer este curso, para que eu pudesse usar projetos inteligentes que outras pessoas criaram e descobriram que funcionam bem, como visitantes do AST e o método de descida recursiva, embora não tenham sido ensinados no nosso curso.

O que realmente me fez pensar é o custo da abstração. As abstrações podem facilitar a expansão futura ou proteger contra alguns tipos de erros, mas precisam ser levadas em consideração, pois você pode obter três vezes mais código para entender e refatorar, três vezes mais lugares possíveis para erros e menos tempo para testes e mais desenvolvimento. Nosso curso de treinamento foi diferente do mundo real: sabíamos com certeza que nunca tocaríamos no código após o desenvolvimento, isso elimina os benefícios da abstração proativa. No entanto, se eu tivesse que escolher qual compilador estender com uma função arbitrária que você dirá mais adiante, eu escolheria a nossa, mesmo sem considerar minha familiaridade com ela. Só porque ele tem muito menos código para entender e eu poderia escolher a melhor abstração para os requisitos (por exemplo,representação intermediária do comando C ++) quando conheço os requisitos específicos.

Além disso, na minha opinião, a taxonomia das abstrações foi reforçada: existem aquelas que reduzem o código, levando em conta apenas os requisitos atuais, como nosso modelo de visitante, e há abstrações que adicionam código, mas fornecem os benefícios de extensibilidade, depuração ou correção.

Scala


Também conversei com um amigo que fez um projeto no Scala no semestre anterior, mas o projeto e os testes foram exatamente os mesmos. O compilador consistia em 4141 linhas e ~ 160 KB de código, sem contar os testes. Eles passaram em 8 dos 10 testes secretos e 100% de testes abertos e não implementaram nenhuma função adicional. Assim, comparado às nossas linhas 5906 sem funções e testes adicionais, o compilador é 30% menor.

Um dos pequenos fatores de design foi uma abordagem diferente para a análise. O curso permitiu o uso de uma ferramenta de linha de comando para o gerador de tabelas LR. Ninguém o usou, exceto esse time. Isso os impediu de implementar o gerador de tabelas LR. Eles também conseguiram evitar escrever gramática LR com um script Python de 150 linhas que rasparam a página da gramática Java encontrada na Internet e a traduziram para o formato de entrada do gerador. Eles ainda precisavam fazer algum tipo de árvore em Scala, mas, em geral, o estágio de análise chegou a 1073 linhas em comparação com o nosso 1443, embora nosso método de descida por gradiente aqui desse uma vantagem em volume em comparação com todas as outras equipes.

O restante do compilador também era menor que o nosso, sem grandes diferenças óbvias de design, embora eu não tenha me aprofundado no código. Suspeito que isso se deva a diferenças na expressividade de Scala e Rust. Scala e Rust têm recursos de programação semelhantes úteis para compiladores, como correspondência de padrões, mas a memória gerenciada do Scala salva o código necessário para que o verificador de empréstimo trabalhe no Rust. Além disso, Scala tem um açúcar sintático mais variado que o Rust.

OCaml


Como todos os membros de nossa equipe passam por um estágio na Jane Street (empresa de comércio de tecnologia - aprox. Por.), Fiquei especialmente interessado em ver o resultado de outros ex-estagiários da Jane Street que escolheram o OCaml para escrever o compilador.

O compilador era de 10.914 linhas e 377 KB, incluindo uma pequena quantidade de código de teste e nenhum recurso adicional. Eles passaram em 9/10 testes secretos e todos os testes públicos.

Como outros grupos, parece que a principal diferença de tamanho se deve ao uso do analisador LR e da reescrita em árvore para análise, bem como do pipeline de conversão regex-> NFA-> DFA para análise lexical. O front-end (análise lexical, análise, construção AST) é de 5548 linhas e o nosso - 2164, com proporções semelhantes para bytes. Eles também usaram testes para seu analisador, com a expectativa de que ele fosse semelhante aos nossos testes de instantâneo, que colocam a saída esperada fora do código; portanto, seus testes de analisador fizeram ~ 600 linhas do total e a nossa - cerca de 200.

Isso deixa 5366 linhas para o restante do compilador (461 linhas são arquivos de interface com declarações de tipo) e 4642 para nós, a diferença é de apenas 15%, se contarmos seus arquivos de interface e quase o mesmo tamanho, se não contar. Parece que, além de nossas soluções de design de análise, Rust e OCaml parecem igualmente expressivos, exceto que o OCaml precisa de arquivos front-end e o Rust não.

Conclusão


Em geral, estou muito feliz por ter feito essa comparação, aprendi muito e fiquei surpreso várias vezes. Penso que a conclusão geral é que as decisões de design são muito mais importantes que a linguagem, mas a linguagem é importante porque fornece ferramentas para a implementação de projetos diferentes.

Source: https://habr.com/ru/post/pt456638/


All Articles