2 puntos por GN⁺ 2024-11-11 | 1 comentarios | Compartir por WhatsApp
  • Jawsm es un compilador de JavaScript→WebAssembly escrito en Rust, una herramienta experimental que crea binarios WASM autónomos ejecutables sin intérprete
  • Al igual que porffor, crea WASM standalone, pero con una implementación distinta, y convierte la sintaxis de JavaScript en instrucciones WASM usando instrucciones de las propuestas más recientes de WASM GC, manejo de excepciones y optimización de tail calls
  • Actualmente pasa alrededor del 25% de la suite de pruebas test262, y ya implementa scopes/closures, try/catch, async/await y generators, que considera claves para validar su viabilidad
  • Aún no está listo para producción; faltan o están incompletas muchas funciones del lenguaje y tipos integrados, y también faltan RegExp, la mayoría de los builtins y la aritmética de BigInt
  • Los binarios generados tienen poca portabilidad entre runtimes debido a su dependencia de propuestas recientes de WASM, y por ahora se ejecutan sobre Chromium o Node basados en V8 junto con un polyfill de WASIp2

Objetivo y posicionamiento de Jawsm

  • Jawsm es un compilador de JavaScript a WebAssembly que se pronuncia como “awesome”
  • Está escrito en Rust y busca convertir código JavaScript en un binario WASM standalone ejecutable sin intérprete
  • Produce un resultado similar a porffor, pero con un enfoque de implementación diferente
  • Por ahora es una herramienta experimental y no está lista para usarse en producción
    • Faltan muchas funciones del lenguaje JavaScript o están incompletas
    • También faltan muchos tipos integrados y métodos, o están incompletos
  • Su objetivo a largo plazo es ofrecer compatibilidad del 100% con las funciones del lenguaje JavaScript

Por qué se creó Jawsm

  • El proyecto comenzó mientras se trabajaba en Crows, una herramienta de stress testing para ejecutar escenarios de WebAssembly
  • Actualmente Crows solo admite código compilado de Rust a WASM
  • Las pruebas pequeñas suelen ser más fáciles de escribir en un lenguaje interpretado, pero ejecutar un lenguaje de scripting sobre WASM todavía no es ideal
    • Incluir un intérprete hace que el binario mida al menos varios MB y también aumente el uso de memoria
    • O bien hay que usar una variante del lenguaje objetivo, como TinyGo o AssemblyScript
  • La meta es que, aprovechando las propuestas modernas de WASM, sea posible implementar el 100% de las funciones de JavaScript sin un intérprete compilado
  • Parte de la idea de que el propio runtime de WASM ya es un intérprete

Funciones que ya funcionan

  • Jawsm actualmente pasa alrededor del 25% de la suite de pruebas test262
  • Ya están implementadas las 4 funciones clave que se consideraban necesarias para validar la viabilidad del proyecto
    • scopes/closures
    • try/catch
    • async/await
    • generators
  • Otras funciones que deberían funcionar son las siguientes
    • declaraciones y asignaciones var, let, const
    • bucles do..while, while, for, for..in, for..of
    • sentencias switch
    • soporte limitado para break, continue
    • literales de cadena y concatenación de literales de cadena
    • números y operadores básicos +, -, *, /
    • booleanos y operadores booleanos básicos
    • arreglos y la mayoría de las funciones relacionadas con Array
    • object literals
    • palabra clave new
    • async, await
    • soporte limitado para la API de Promise
    • generator functions
    • try/catch
    • soporte muy básico para BigInt

Funciones que aún faltan

  • Los elementos principales que faltan actualmente son los siguientes
    • la mayoría de los builtins
    • la mayoría de los métodos de los builtins existentes
    • expresiones RegExp
    • aritmética de BigInt
  • El plan de los siguientes pasos se centra en implementar estas funciones
    • soporte básico para regexp
      • literales RegExp
      • funciones muy básicas del objeto RegExp
    • literales BigInt y soporte básico de BigInt
    • mejor casting automático al usar comparaciones de igualdad o varios operadores
    • más funciones de builtins básicos como arrays, strings, etc.

Entorno de ejecución y limitaciones

  • Como Jawsm usa varias propuestas relativamente recientes de WASM, los binarios generados todavía no tienen alta portabilidad entre runtimes
  • La implementación objetivo está pensada con WASIp2 en mente
  • Wasmtime es un runtime capaz de ejecutar components y WASIp2, pero no soporta algunos elementos de WASM GC ni aspectos como exception handling que usa Jawsm
  • Para facilitar el desarrollo hasta que los runtimes alcancen las propuestas estandarizadas, se usa V8
    • Se usa V8 a través de Chromium o Node
    • Las funciones necesarias de WASIp2 se complementan con un polyfill en JavaScript
  • El repositorio incluye un script run.js para ejecutar los binarios generados por Jawsm
  • El objetivo final es que pueda ejecutarse en cualquier runtime que implemente WASM GC, exception handling y la API de WASIp2
    • O también mediante el uso de un polyfill de WASIp2

Cómo usarlo

  • Por ahora no se recomienda usarlo salvo para contribuir al proyecto
  • Después de clonar el repositorio, se puede usar execute.sh de la siguiente manera
./execute.sh --cargo-run path/to/script.js
  • Este comando genera un archivo WAT, lo compila a binario y luego lo ejecuta con Node.js
  • Las herramientas necesarias son las siguientes
    • cargo de Rust
    • una versión relativamente reciente de wasm-tools
    • Node.js v23.0.0 o superior
  • Si se pasa la opción --cargo-run, primero compila el proyecto con cargo run y luego lo ejecuta
  • Si se ejecuta sin --cargo-run, intentará usar el build release, por lo que antes hay que ejecutar cargo build --release

Funcionamiento interno

  • Jawsm convierte la sintaxis de JavaScript en instrucciones WASM
  • El proceso de conversión aprovecha instrucciones de las siguientes propuestas de WASM
    • WASM GC

      • exception handling
      • tail call optimizations
      • El código Rust transforma el script y, junto con ello, se usa un conjunto de tipos y funciones para trasladar la semántica de JavaScript a WASM
      • La mayoría de las instrucciones WASM se generan usando tarnik
      • tarnik es una macro de Rust que genera instrucciones WASM basada en una sintaxis similar a Rust

Ejemplo de manejo de scope y closure

  • WASM soporta function references, structs y arrays, pero no proporciona directamente la semántica de scope de JavaScript
  • Jawsm genera código WASM adicional para imitar el comportamiento de los scopes de JavaScript
  • El código JavaScript de ejemplo es el siguiente
let a = "foo";

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

bar();
  • En JavaScript, la definición de una función hereda el scope donde fue definida, por lo que bar() debe poder acceder a la variable a
  • El flujo de transformación es aproximadamente el siguiente
    • se crea un global scope sin parent
    • se declara la variable a con "foo" en el scope actual
    • al crear el objeto función bar, también se guarda una referencia al scope donde fue definida
    • al ejecutar la función por dentro, se crea un nuevo scope pero se mantiene la referencia parentScope
    • retrieve(scope, "a") busca a en el scope actual y en todos los parent scope
    • se obtiene bar del scope actual y se invoca

Licencia

  • El código se distribuye bajo la licencia Apache 2.0

1 comentarios

 
GN⁺ 2024-11-11
Opiniones de Hacker News
  • Uso realmente ingenioso de la propuesta WASM GC.
    Hasta ahora, los compiladores JS→WASM básicamente cargaban todo el motor de JS, pero es la primera vez que veo un intento de mapear directamente las estructuras de JS a funcionalidades nativas de WASM.

    • Porffor https://porffor.dev/ y Static Hermes https://hermesengine.dev/ también parecen seguir un enfoque de compilación.
      Sería interesante compararlos con Jaws.
    • Así es. Aunque, para ser sinceros, es más bien un uso ingenioso que surgió porque no soy lo bastante inteligente como para escribir un intérprete completo sobre WASM.
  • Hace tiempo hice un lenguaje muy cercano a TypeScript como compilador para ARM embebido.
    Era mucho más cercano a TypeScript que AssemblyScript, y algunas de las técnicas que usé entonces podrían servir.
    https://www.microsoft.com/en-us/research/uploads/prod/2019/09/static-typescript-draft2.pdf

  • ¿Es cierta la frase “Me encanta usar Rust, pero también sé que no es un lenguaje ampliamente popular”?
    Rust está recibiendo muchísimo hype y hoy parece usarse en todas partes.

    • Todavía no he encontrado empleos de Rust que no estén relacionados con criptomonedas.
      En mi país, Estonia, según las bolsas de trabajo locales que usa la gente, hay 0 puestos locales, así que en un sentido realmente importante difícilmente puede decirse que sea popular.
    • Que tenga mucho hype y visibilidad en redes no significa necesariamente que se use ampliamente.
      Depende de cómo se defina “ampliamente”, pero según índices como StackOverflow y PyPL, además de estadísticas de GitHub, el uso de Rust parece estar en torno al 5–10% del de JavaScript o Python.
  • Si “al final estoy bastante seguro de poder cubrir el 100% de la especificación de JavaScript”, ¿hay resultados de test262_runner.rb?
    Conocí test262 por una presentación del autor de Porffor, y estaría bueno ver en el README algún indicador de progreso como https://github.com/CanadaHonk/porffor?tab=readme-ov-file#test262.
    Es un buen proyecto.

    • Actualmente pasa alrededor del 12% de las pruebas, pero todavía quedan muchas partes fáciles de implementar.
      Especialmente considerando que el proyecto empezó hace apenas 2 semanas; claro que eso no significa que pueda llegar linealmente al 100%.
      Queda una larga cola de tipos y funciones integradas, pero como empecé implementando las “partes difíciles”, todavía no hice algunas de las fáciles.
      Por ejemplo, solo implementé la sintaxis mínima necesaria para ejecutar el harness de test262, como condicionales y bucles while; todavía no implementé for, for in, for of, do while ni expresiones condicionales como switch.
      Esas se pueden agregar de forma casi idéntica a las implementaciones existentes de if/else y while.

      Cuando termine de implementar await y los generadores, quedarán resueltos los últimos conceptos semánticos difíciles, así que después voy a implementar esas partes fáciles.
      Es difícil decir cuánto subirá la cobertura, pero por ejemplo ahora fallan 1200 pruebas porque la sintaxis object["foo"] no está implementada.
      object.foo funciona, pero object["foo"] no.
      Eso no significa que esas 1200 vayan a pasar automáticamente, pero a menudo cientos de pruebas fallan por omisiones de sintaxis relativamente simples como esta.

      Definitivamente quiero agregar también un gráfico bonito como el de Porffor.

  • Incluso después de leer el README.md del proyecto todavía no me queda claro: ¿cuál es el modo de uso esperado?
    Me interesa saber cómo interactúa el código WASM generado con qué runtime.
    También me pregunto si es una herramienta compatible tanto con navegadores como con otros runtimes de WASM, o si solo funciona con el runtime vinculado al proyecto.

    Relacionado con eso, ¿cómo reacciona cuando dentro del código JavaScript encuentra API web o identificadores globales definidos solo en un entorno específico, por ejemplo identificadores globales de navegadores modernos o de Node.js?
    Si no apunta a esos entornos, ¿cómo debería hacerse el I/O?

    • Buenas preguntas; voy a agregar más detalles al README.
      Este proyecto apunta principalmente al uso de WebAssembly en servidores.
      Ejecutar JavaScript dentro de WebAssembly dentro de JavaScript me parece de poco sentido, aunque con el tiempo podría ser útil para aislar plugins de frontend en un sandbox.

      Ya sea que se ejecute en el navegador o en runtimes backend como WasmTime o WasmEdge, actualmente ejecutar JavaScript dentro de WebAssembly no es ideal.
      Hay que compilar a WASM un motor de JS como V8 o SpiderMonkey y ejecutar los scripts encima, o conformarse con un lenguaje “casi JavaScript” como AssemblyScript.
      Esto se vuelve un factor limitante para ejecutar cargas de trabajo de servidor.
      Por ejemplo, Fastly usa SpiderMonkey en sus workers WASM, y solo un hello world consume 5–10 MB de memoria por instancia.
      En cambio, Shopify usa WASM para personalización del lado del servidor en tiendas y limitó los binarios WASM a 250 KB o menos; con ese tamaño es difícil incluir cualquier intérprete.
      Por eso el lenguaje “recomendado” terminó siendo AssemblyScript, y aquí explican el motivo: https://shopify.engineering/shopify-webassembly

      Históricamente, esta situación se debe a que WASM era un runtime muy simple.
      Era relativamente fácil compilar código C a WASM, como se compila a código máquina, pero aunque WebAssembly en sí es una especie de intérprete, no era fácil interpretar encima de él lenguajes de más alto nivel.

      Ahora, con la estandarización de nuevas propuestas como el soporte de recolección de basura y el soporte de manejo de excepciones, WebAssembly se está convirtiendo en un intérprete mucho más potente, con funcionalidades como estructuras, arreglos y referencias a funciones.

Jaws aprovecha esto para convertir código JS en código WASM y hacer que WASM interprete el código resultante sin un motor JS como SpiderMonkey.
En la práctica, el binario generado por Jaws probablemente pueda ser de menos de 50 KB, en contraste con los 10 MB del enfoque de compilar SpiderMonkey a WASM y ejecutar scripts sobre él.
El uso de memoria también debería bajar mucho.
Para empresas como Fastly, esto significa poder reducir el uso de memoria y los costos de servidor en órdenes de magnitud; para empresas como Shopify, significa permitir que los autores de plugins de backend aprovechen código JavaScript existente, por ejemplo paquetes NPM y el ecosistema JavaScript.

El único runtime que usa el proyecto es **WebAssembly**.  
El código generado se compone, en general, de unas 3 mil líneas de código WAT de este archivo [https://github.com/drogus/jaws/blob/main/src/wat/template.wat](<https://github.com/drogus/jaws/blob/main/src/wat/template.wat>;) y de la parte del código JS del usuario que fue transformada.  
Por ejemplo, en un programa muy simple como `"console.log('foo')"`, toda la parte “generada” es solo esto: [https://gist.github.com/drogus/1c49c25ed0b14804b2f27e10d2a79928/…](<https://gist.github.com/drogus/1c49c25ed0b14804b2f27e10d2a79928/…;)  
Básicamente prepara el argumento con `new_static_string` y luego llama a `console.log`.  
Por ahora se necesita algo de código glue del lado del host, pero eventualmente debería ser posible ejecutar estos binarios en cualquier runtime que soporte WASIp2, WASM GC y la propuesta de manejo de excepciones.

El soporte para APIs web o identificadores globales específicos del entorno aún no está implementado, pero se puede explicar cómo funcionaría.  
La idea es soportar las **APIs de Node.js** mediante WASI.  
WASI es un estándar para que los programas WASM se comuniquen con el mundo exterior.  
Por ejemplo, define un conjunto estándar de funciones que pueden usarse para enviar solicitudes HTTP, escribir en STDOUT, leer/escribir archivos, etc.  
Así que, cuando se llegue a APIs como `fetch` o `fs`, deberían funcionar en runtimes que soporten WASI preview2.  
Los navegadores también podrían soportarlo con polyfills, pero en ese caso el soporte de I/O sería más a medida.  
Si permites que un programa WASM lea o escriba archivos, tendrías que proporcionar un mecanismo como, por ejemplo, guardarlos en localStorage, usar una base de datos SQLite compilada a WASM o incluso enviarlos a algún lugar como S3.
  • Se siente como si se acercara la “ejecución de JS sin runtime de navegador”.
    Creo que Porffor, Jaws o algún otro proyecto finalmente lo logrará.

  • Me gusta mucho este enfoque.
    En vez de intentar generar binarios directamente, al apuntar directamente a WASM puedes depender de WASM GC y del soporte asíncrono que parece que vendrá incluido en WASI 0.3.

  • ¿Cómo manejan las diferencias de codificación de cadenas y las utilidades relacionadas?
    Según entiendo vagamente, WASM soporta UTF-8 y JS puede soportar incluso UTF-16 potencialmente roto.

    • Como con los conjuntos de instrucciones de CPU, la máquina abstracta de WASM no tiene concepto de cadenas ni de codificación.
      Solo son bytes en memoria lineal, y puedes implementar la codificación que quieras.

      WASM especifica UTF-8 como codificación de nombres en el formato de archivo, pero eso no tiene relación con la máquina virtual en tiempo de ejecución.

  • Algunas personas también lo llamarían compilador.
    En todo caso, está bien hecho.

    • Al escribir el título no me di cuenta para nada de que sonaba un poco raro.
  • ¿Esto es más rápido que simplemente ejecutar el mismo código en JS, o es para interoperar con otros lenguajes?

    • En esta etapa es difícil decirlo, pero veo muy poco probable que llegue a ser más rápido que SpiderMonkey o V8 con JIT activado.
      Los compiladores JavaScript modernos optimizan bastante bien las rutas que se ejecutan con frecuencia mediante JIT.
      El caso de uso de este proyecto es permitir ejecutar JavaScript en un entorno sandbox de WebAssembly.
      Por ejemplo, Shopify permite extender código de backend con WebAssembly, pero limita el tamaño del binario a 250 KB.
      Con ese tamaño, hoy es difícil usar JavaScript, porque incluso un intérprete simple como QuickJS termina pesando varios MB cuando se compila a WASM.