- 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
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.
Sería interesante compararlos con Jaws.
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.
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.
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
awaity 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.foofunciona, peroobject["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.
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.
¿Esto es más rápido que simplemente ejecutar el mismo código en JS, o es para interoperar con otros lenguajes?
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.