2 pontos por GN⁺ 2024-11-11 | 1 comentários | Compartilhar no WhatsApp
  • Jawsm é um compilador experimental de JavaScript→WebAssembly escrito em Rust que cria binários WASM independentes executáveis sem interpretador
  • Assim como o porffor, ele cria WASM standalone, mas com uma abordagem de implementação diferente, convertendo sintaxe JavaScript em instruções WASM usando comandos das propostas mais recentes de WASM GC, tratamento de exceções e otimização de tail call
  • Atualmente passa em cerca de 25% da suíte de testes test262, e os recursos considerados importantes para verificar a viabilidade — scopes/closures, try/catch, async/await e generators — já foram implementados
  • Ainda não está pronto para produção; muitos recursos da linguagem e tipos embutidos estão ausentes ou incompletos, e também faltam RegExp, a maioria dos builtins e aritmética com BigInt
  • Os binários gerados têm baixa portabilidade entre runtimes por dependerem de propostas recentes de WASM; por enquanto, o uso atual é executá-los em Chromium ou Node baseados em V8 com um polyfill de WASIp2

Objetivo e posicionamento do Jawsm

  • Jawsm é um compilador de JavaScript para WebAssembly, pronunciado como “awesome”
  • Foi escrito em Rust e tem como objetivo transformar código JavaScript em um standalone WASM binary executável sem interpretador
  • Produz um resultado parecido com o porffor, mas com uma abordagem de implementação diferente
  • No momento, é uma ferramenta experimental e não está pronta para uso em produção
    • Muitos recursos da linguagem JavaScript estão ausentes ou incompletos
    • Muitos tipos e métodos embutidos também estão ausentes ou incompletos
  • O objetivo de longo prazo é oferecer 100% de suporte aos recursos da linguagem JavaScript

Por que o Jawsm foi criado

  • O projeto começou durante o trabalho em Crows, uma ferramenta de stress test para executar cenários WebAssembly
  • Atualmente, o Crows só suporta código compilado de Rust para WASM
  • Testes pequenos costumam ser mais fáceis de escrever em linguagens interpretadas, mas hoje executar uma scripting language sobre WASM ainda não é o ideal
    • Incluir um interpretador faz o binário ter no mínimo alguns MB e também aumenta o uso de memória
    • Ou então é preciso usar uma variação da linguagem-alvo, como TinyGo ou AssemblyScript
  • A proposta é que, aproveitando propostas modernas de WASM, seja possível implementar 100% dos recursos do JavaScript sem um interpretador compilado
  • Isso parte do princípio de que o próprio runtime WASM já é um interpretador

Recursos que já funcionam

  • Atualmente, o Jawsm passa em cerca de 25% da suíte de testes test262
  • Os 4 recursos principais para verificar a viabilidade do projeto já foram implementados
    • scopes/closures
    • try/catch
    • async/await
    • generators
  • Outros recursos que já devem funcionar incluem:
    • declarações e atribuições var, let, const
    • loops do..while, while, for, for..in, for..of
    • instrução switch
    • suporte limitado a break e continue
    • string literals e concatenação de string literals
    • números e operadores básicos +, -, *, /
    • boolean e operadores booleanos básicos
    • arrays e a maioria das funções relacionadas a Array
    • object literals
    • palavra-chave new
    • async, await
    • suporte limitado à API de Promise
    • generator functions
    • try/catch
    • suporte bem básico a BigInt

Recursos que ainda faltam

  • Os principais itens ausentes no momento são:
    • a maioria dos builtins
    • a maior parte dos métodos dos builtins existentes
    • expressões RegExp
    • aritmética com BigInt
  • Os próximos passos do plano se concentram em implementar:
    • suporte básico a regexp
      • RegExp literals
      • funções bem básicas do objeto RegExp
    • BigInt literals e suporte básico a BigInt
    • automatic casting melhor ao usar testes de igualdade ou vários operadores
    • mais funções dos builtins básicos, como arrays, strings etc.

Ambiente de execução e limitações

  • Como o Jawsm usa algumas propostas de WASM relativamente recentes, os binários gerados ainda não têm alta portabilidade entre runtimes
  • A implementação-alvo foi pensada com WASIp2 em mente
  • O Wasmtime é um runtime capaz de executar components e WASIp2, mas não oferece suporte a alguns elementos usados pelo Jawsm, como partes de WASM GC e tratamento de exceções
  • Até que os runtimes acompanhem as propostas padronizadas, o projeto usa V8 para facilitar o desenvolvimento
    • O V8 é usado via Chromium ou Node
    • Os recursos WASIp2 necessários são complementados com um polyfill em JavaScript
  • O repositório inclui o script run.js para executar os binários gerados pelo Jawsm
  • No fim, o objetivo é que ele possa rodar em qualquer runtime que implemente WASM GC, tratamento de exceções e a API WASIp2
    • ou também por meio de um polyfill de WASIp2

Como usar

  • No momento, o uso não é recomendado, a menos que seja para contribuir
  • Depois de clonar o repositório, é possível usar execute.sh assim:
./execute.sh --cargo-run path/to/script.js
  • Esse comando gera um arquivo WAT, compila para binário e depois executa com Node.js
  • As ferramentas necessárias são:
    • cargo do Rust
    • uma versão relativamente recente de wasm-tools
    • Node.js v23.0.0 ou superior
  • Ao passar a opção --cargo-run, o projeto é primeiro compilado com cargo run antes da execução
  • Se executar sem --cargo-run, ele tentará rodar o build de release, então antes é preciso executar cargo build --release

Como funciona internamente

  • O Jawsm converte sintaxe JavaScript em instruções WASM
  • O processo de conversão usa instruções das seguintes propostas de WASM:
    • WASM GC

      • tratamento de exceções
      • otimizações de tail call
      • o código Rust transforma o script, e junto disso é usado um conjunto de tipos e funções para transportar a semântica de JavaScript para WASM
      • a maioria das instruções WASM é gerada usando tarnik
      • tarnik é uma macro Rust que gera instruções WASM com base em uma sintaxe no estilo Rust

Exemplo de tratamento de scope e closure

  • O WASM oferece suporte a function references, structs e arrays, mas não fornece diretamente a semântica de scope do JavaScript
  • O Jawsm gera código WASM adicional para imitar o comportamento de scope do JavaScript
  • O código JavaScript de exemplo é o seguinte:
let a = "foo";

function bar() {
  console.log(a);
}

bar();
  • Em JavaScript, a definição de uma função herda o scope em que ela foi definida, então bar() deve poder acessar a variável a
  • O fluxo de conversão é aproximadamente este:
    • cria um global scope sem parent
    • declara a variável a com "foo" no scope atual
    • ao criar o objeto de função bar, também armazena junto uma referência ao scope em que a função foi definida
    • durante a execução da função, cria um novo scope, mas mantém a referência parentScope
    • retrieve(scope, "a") procura a no scope atual e em todos os parent scopes
    • recupera bar do scope atual e o chama

Licença

  • O código é distribuído sob a licença Apache 2.0

1 comentários

 
GN⁺ 2024-11-11
Comentários do Hacker News
  • Usaram a proposta de WASM GC de forma realmente inteligente.
    Até agora, compiladores JS→WASM basicamente embarcavam um motor JS inteiro; é a primeira vez que vejo uma tentativa de mapear a estrutura do JS diretamente para recursos primitivos do WASM.

    • Porffor https://porffor.dev/ e Static Hermes https://hermesengine.dev/ também parecem adotar uma abordagem de compilação.
      Seria interessante compará-los com o Jaws.
    • Sim. Mas, sendo sincero, isso está mais para um uso inteligente que surgiu porque eu não sou esperto o suficiente para escrever um interpretador completo em cima de WASM.
  • Há algum tempo, criei uma linguagem quase próxima de TypeScript como compilador para ARM embarcado.
    Ela era muito mais próxima de TypeScript do que AssemblyScript, e algumas das técnicas usadas naquela época podem ajudar.
    https://www.microsoft.com/en-us/research/uploads/prod/2019/09/static-typescript-draft2.pdf

  • A frase “adoro muito usar Rust, mas também sei que não é uma linguagem amplamente popular” está certa?
    Rust é extremamente hypado e parece estar sendo usado em todos os lugares hoje em dia.

    • Ainda não consegui encontrar uma vaga de Rust que não seja relacionada a criptomoedas.
      No meu país, a Estônia, há 0 vagas locais nos painéis de emprego locais que as pessoas usam; então, em qualquer sentido realmente importante, é difícil dizer que seja popular.
    • O fato de algo ser hypado nas redes sociais e bem visível não significa necessariamente que seja amplamente usado.
      Depende de como você define “amplamente”, mas olhando índices como StackOverflow e PyPL e estatísticas do GitHub, o uso de Rust parece estar em torno de 5% a 10% do JavaScript ou Python.
  • Se “no fim, estou bastante confiante de que conseguiremos cobrir 100% da especificação do JavaScript”, há resultados do test262_runner.rb?
    Fiquei sabendo do test262 por uma apresentação do autor do Porffor, e seria bom ter uma indicação de progresso no README como https://github.com/CanadaHonk/porffor?tab=readme-ov-file#test262.
    É um bom projeto.

    • Atualmente ele passa em cerca de 12% dos testes, mas ainda há muitas partes fáceis de implementar.
      Isso vale especialmente considerando que o projeto começou há apenas 2 semanas; claro, isso não quer dizer que seja possível chegar a 100% de forma linear.
      Ainda resta a longa cauda de tipos e funções embutidos, mas como comecei implementando as “partes difíceis”, algumas partes fáceis ainda não foram feitas.
      Por exemplo, implementei na sintaxe apenas o suficiente de condicionais e loops while para rodar o harness do test262; ainda não implementei expressões condicionais como for, for in, for of, do while e switch.
      Elas podem ser adicionadas de forma quase igual às implementações existentes de if/else e while.

      Quando eu terminar a implementação de await e generators, os últimos conceitos semânticos difíceis estarão resolvidos, então depois pretendo implementar essas partes fáceis.
      É difícil dizer quanto a cobertura vai subir, mas, por exemplo, hoje 1200 testes falham porque a sintaxe object["foo"] não está implementada.
      object.foo funciona, mas object["foo"] não.
      Isso não quer dizer que esses 1200 testes vão passar automaticamente, mas muitas vezes centenas de testes falham por ausências de sintaxe relativamente simples como essa.

      Também quero muito colocar um gráfico legal como o do Porffor.

  • Mesmo lendo o README.md do projeto, ainda não entendi bem: qual é a forma de uso esperada?
    Tenho curiosidade sobre como o código WASM gerado interage com qual runtime e de que maneira.
    Também gostaria de saber se é uma ferramenta compatível tanto com navegadores quanto com outros runtimes WASM, ou se só funciona no runtime vinculado ao projeto.

    Relacionado a isso, como ele reage quando encontra, dentro do código JavaScript, APIs da Web ou identificadores globais definidos apenas em ambientes específicos, por exemplo identificadores globais de navegadores modernos ou do Node.js?
    Se esses ambientes não são o alvo, como deve ser feito I/O?

    • Ótimas perguntas; vou adicionar mais detalhes ao README.
      Este projeto mira principalmente o uso de WebAssembly no servidor.
      Rodar JavaScript em WebAssembly dentro de JavaScript me parece ter pouco sentido, mas, com o tempo, pode ser útil para sandboxing de plugins no front-end.

      Seja rodando no navegador, seja em runtimes de backend como WasmTime ou WasmEdge, executar JavaScript dentro de WebAssembly hoje não é ideal.
      Ou você compila um motor JS como V8 ou SpiderMonkey para WASM e roda scripts sobre ele, ou se contenta com uma linguagem “quase JavaScript” como AssemblyScript.
      Isso se torna um fator limitante para executar workloads de servidor.
      Por exemplo, a Fastly usa SpiderMonkey em workers WASM, e mesmo um hello world consome 5 a 10 MB de memória por instância.
      Já a Shopify usa WASM para customizações do lado do servidor de lojas e limita os binários WASM a 250 KB ou menos; nesse tamanho, é difícil incluir qualquer interpretador.
      Por isso, a linguagem “recomendada” acabou sendo AssemblyScript, e o motivo é explicado aqui: https://shopify.engineering/shopify-webassembly

      Historicamente, essa situação existiu porque WASM era um runtime muito simples.
      Era relativamente fácil compilar código C para WASM, como se compila para código de máquina, mas, embora o próprio WebAssembly seja uma espécie de interpretador, não era fácil interpretar linguagens de mais alto nível sobre ele.

      Agora, com novas propostas como suporte a garbage collection e suporte a exceções sendo padronizadas, o WebAssembly está se tornando um interpretador muito mais poderoso, com recursos como structs, arrays e referências a funções.

O Jaws aproveita esse ponto para converter código JS em código WASM e fazer com que o WASM interprete o código resultante sem um mecanismo JS como o SpiderMonkey
Na prática, o binário gerado pelo Jaws provavelmente pode ter menos de 50 KB, em contraste com os 10 MB da abordagem de compilar o SpiderMonkey para WASM e executar scripts sobre ele
O uso de memória também deve cair bastante
Para empresas como a Fastly, isso significa poder reduzir o uso de memória e os custos de servidor em ordens de grandeza; para empresas como a Shopify, significa permitir que autores de plugins de backend aproveitem código JavaScript existente, como pacotes NPM e o ecossistema JavaScript

O runtime usado pelo projeto é **apenas WebAssembly**  
O código gerado é composto, em geral, por cerca de 3 mil linhas de código WAT deste arquivo [https://github.com/drogus/jaws/blob/main/src/wat/template.wat](<https://github.com/drogus/jaws/blob/main/src/wat/template.wat>;) e pela parte convertida do código JS do usuário  
Por exemplo, em um programa muito simples como `"console.log('foo')"`, toda a parte “gerada” é apenas isto: [https://gist.github.com/drogus/1c49c25ed0b14804b2f27e10d2a79928/…](<https://gist.github.com/drogus/1c49c25ed0b14804b2f27e10d2a79928/…;)  
Isso basicamente prepara o argumento com `new_static_string` e depois chama `console.log`  
Hoje ainda é necessário um pouco de código de cola no lado do host, mas, no fim, será possível executar esses binários em qualquer runtime que ofereça suporte a WASIp2, WASM GC e à proposta de tratamento de exceções

O suporte a APIs Web ou identificadores globais específicos de ambiente ainda não foi implementado, mas dá para explicar como funcionará  
A intenção é oferecer suporte às **APIs do Node.js** por meio do WASI  
WASI é um padrão para comunicação entre programas WASM e o mundo externo  
Por exemplo, ele define um conjunto de funções padrão que podem ser usadas para enviar requisições HTTP, escrever em STDOUT, ler/gravar arquivos etc.  
Portanto, ao chegar a APIs como `fetch` ou `fs`, elas devem funcionar em runtimes com suporte ao WASI preview2  
Navegadores também podem ser suportados com polyfills, mas nesse caso o suporte a I/O fica mais personalizado  
Se você permitir que um programa WASM leia ou grave arquivos, precisará fornecer algum mecanismo, por exemplo salvar no localStorage, usar um banco de dados SQLite compilado para WASM, ou até enviar para algum lugar como o S3
  • Parece que “executar JS sem runtime de navegador” está se aproximando
    Acho que Porffor, Jaws ou algum outro projeto acabará dando certo

  • Gostei muito desta abordagem
    Em vez de tentar gerar binários diretamente, ao compilar mirando diretamente em WASM, dá para depender do WASM GC e do suporte a assíncrono que aparentemente deve entrar no WASI 0.3

  • Como são tratadas as diferenças de codificação de strings e os utilitários relacionados?
    Pelo que entendo vagamente, WASM oferece suporte a UTF-8, enquanto JS também oferece suporte a UTF-16 potencialmente inválido

    • Como um conjunto de instruções de CPU, a máquina abstrata do WASM não tem conceito de strings nem de codificação
      São apenas bytes na memória linear, e você implementa a codificação que quiser

      O WASM especifica UTF-8 como codificação de nomes no formato de arquivo, mas isso não tem relação com a máquina virtual em tempo de execução

  • Algumas pessoas chamariam isso de compilador
    De qualquer forma, ficou bem feito

    • Ao escrever o título, nem percebi que a formulação estava um pouco estranha
  • Isso é mais rápido do que simplesmente executar o mesmo código em JS, ou é para interoperabilidade com outras linguagens?

    • Neste estágio é difícil dizer, mas acho muito improvável que fique mais rápido do que SpiderMonkey ou V8 com JIT ativado
      Compiladores JavaScript modernos otimizam muito bem, via JIT, os caminhos executados com frequência
      O objetivo deste projeto é permitir executar JavaScript em um ambiente sandbox WebAssembly
      Por exemplo, a Shopify permite estender código de backend com WebAssembly, mas limita o tamanho do binário a 250 KB
      Com esse tamanho, hoje é difícil usar JavaScript, porque mesmo um interpretador simples como o QuickJS, quando compilado para WASM, fica com alguns MB