Generating JS
Objetivos
Escriba un traductor desde el lenguaje Egg hasta el lenguaje JavaScript.
Reutilice su parser para crear los árboles sintácticos. Añada funciones de recorrido de los ASTs para ir generando el código JS.
El ejecutable
Añada una opción -j --js
a su ejecutable o bien en su directorio bin
un nuevo ejecutable egg2js.js
que permita hacer la traducción
Un ejemplo sencillo: examples/times.egg
Cuando al programa se le proporciona una entrada con expresiones Egg de tipo apply como estas:
$ cat examples/times.egg
print(
+(3,
*(4,
5
)
),
-(9,3)
)
debería dar como salida un fichero examples/times.js
con una salida parecida a esta:
➜ crguezl-egg-2-js-2021 git:(main) ✗ bin/egg2js.js -j examples/times.egg
➜ crguezl-egg-2-js-2021 git:(main) ✗ cat examples/times.js
const Egg = require("runtime-support");
Egg.print((3 + (4 * 5)), (9 - 3));
Añadiendo Métodos a las Clases de los Nodos del AST
Una aproximación que he seguido a la hora de hacer esta práctica es añadir métodos generateJS
a cada uno de los diferentes tipos de nodos del AST que se encarguen de la generación del código JS correspondiente a ese tipo de nodo:
class Apply {
...
generateJS() {
let argsTranslations = this.args.map(x => x.generateJS())
if (this.operator.type === 'word') {
...
}
else {
...
}
}
}
Strategy Pattern Again: Un mapa de generadores de JS
Para facilitar la generación del código JS puede serle útil seguir el Strategy Pattern y tener un módulo que exporta un mapa/hash generateJS
cuyas claves son
las mismas que en specialForms
y topEnv
y los valores son las correspondientes
funciones de generación de código JS. De esta manera evitamos en lo posible violar el principio OPEN/CLOSED:
const util = require('util');
const ins = x => util.inspect(x, { depth: null});
let generateJS = Object.create(null);
const ARITHM_OPERATORS = [ "+", "-", ... ">>>" ];
ARITHM_OPERATORS.forEach(op => {
generateJS[op] = function([left, right]) {
return `(${left} ${op} ${right})`;
}
});
generateJS['print'] = function(...args) {
...
}
generateJS['do'] = function(statements) {
...
}
generateJS['def'] = function([variable, initexpression]) {
...
}
generateJS['='] = generateJS['set'] = function([variable, expression]) {
...
}
generateJS['fun'] = function(parameters) {
...
}
...
module.exports = {
generateJS,
...
};
Consideraciones semánticas
El print
en mi versión de Egg retorna los argumentos con los que se llamó, mientras que en JS console.log
retorna undefined
.
Nótese que entonces no puedo traducir print(...)
directamente a un console.log(...)
.
Por eso print(...)
se podría traducir siguiendo este esquema:
generateJS['print'] = function(...args) {
return `
(() => {
let tmp = $args;
console.log(...tmp);
return ${tmp};
})()`
}
en general, procure que cualquier programa JS resultante de la traducción de un programa Egg produzca los mismos resultados que cuando el programa Egg es interpretado.
Librería de Soporte en Ejecución (Runtime Library)
Puede serle útil escribir una librería runtime-support.js
con funciones que den soporte a la ejecución de los programas JS traducidos. Algo así:
➜ crguezl-egg-2-js-2021 git:(main) ✗ cat lib/runtime-support.js
function print(...args) {
console.log(...args);
return args;
}
...
module.exports = {
print,
...
}
Por ejemplo, otra traducción para print(4)
sería:
const egg = require("runtime-support");
egg.print(4);
que queda mas simple que la anterior solución y que garantiza que egg.print(4)
retorna un 4
.
Un ejemplo mas complejo: Manejo de Ámbitos
Cuando se declaren variables y funciones y se creen nuevos ámbitos como en este ejemplo (supongamos que además de las funciones el do
tiene su propio ámbito):
✗ cat examples/hello-scope.egg
print("computed value = ",
do(
def(x,4),
def(inc, fun(w, do(
def(y, 999),
+(w,1)
) # do
) # fun
),# def
def(z,-1),
set(x, inc(x))
)
)
La traducción debe producir el código JavaScript equivalente:
✗ bin/egg.js -j examples/hello-scope.egg
✗ cat examples/hello-scope.js
const Egg = require("runtime-support");
Egg.print("computed value = ", (() => {
let $x = 4
let $inc = function($w) {
if (arguments.length !== 1) throw Error("Function called with wrong number of arguments");
return (() => {
let $y = 999
return ($w + 1)
})()
}
let $z = -1
return $x = $inc($x)
})());
Note como prefijamos las variables del fuente con “$
” de manera que def(x,4)
se convierte en:
let $x = 4
esto es para que las variables traducidas no colisionen contra variables auxiliares que pudieramos necesitar introducir para dar soporte a la traducción.
Obsérvese como es la traducción que hemos hecho de un do
:
do(
def(y, 999),
+(w,1)
) # do
lo hemos convertido en:
(() => {
let $y = 999
return ($w + 1)
})()
Vea como se crea el ámbito mediante una función anónima (() => { ... })()
que se ejecuta sobre la marcha de manera que retorna la última expresión evaluada.
No hemos hecho uso de una traducción directa de un do
por una sentencia compuesta
{ ... }
y nos hemos tomado estas molestias para respetar la semántica de Egg.
Traduciendo applys sobre applys
Nótese que en Egg el operador de un apply puede ser a su vez un apply como en este ejemplo con la expresión f(2)(4)
:
➜ crguezl-egg-2-js-2021 git:(operator) ✗ cat examples/funfun.egg
do(
def(f, fun(x, fun(y, +(x,y)))),
print(f(2)(4)) # 6
)
Es por tanto necesario traducir correctamente el operador:
➜ crguezl-egg-2-js-2021 git:(main) ✗ cat examples/funfun.js
const Egg = require("runtime-support");
(() => {
let $f = function($x) {
if (arguments.length !== 1) throw Error("Function called with wrong number of arguments");
return function($y) {
if (arguments.length !== 1) throw Error("Function called with wrong number of arguments");
return ($x + $y)
}
}
return Egg.print($f(2)(4));
})()
Aspecto Visual del Código Generado
Puede usar algún módulo como este:
- js-beautify npm module
para mejorar el aspecto visual del código de salida
Simplificaciones
No hace falta añadir comprobaciones de errores de ámbito ni de tipo en esta fase. Esto es, se asume que el código Egg es correcto y las variables han sido declaradas antes de su uso y que son usadas de acuerdo al tipo del valor que contienen.
No hace falta que traduzca el total de su lenguaje Egg, sólo las funcionalidades mas importantes. Deberían funcionar al menos los ejemplos usados en esta página.
Procure mantener la semántica de Egg pero no se complique si es difícil. Tanto si le resulta muy difícil o si decide cambiarla en algún punto, hágalo notar en la documentación.
Referencias
Rúbrica
Incidencias para el Project Board para la prácticaGenerating JS
Se añadió una opción -j –js al ejecutable o bien en su directorio bin hay un nuevo ejecutable egg2js.js que permite hacer la traducción
Traduce correctamente todos los ejemplos en la descripción de la práctica
La solución presentada respeta el principio OPEN/CLOSED
En lo posible se respeta la semántica de Egg y en dodne no se documenta apropiadamente
El número de constructos Egg traducidos es un criterio en la calificación de esta práctica
Opcional: Se proporciona información de localización (offset, etc.)
Se ha hecho un buen uso del versionado semántico en la evolución del módulo
El superproyecto está correctamente estructurado usando submódulos
Se ha publicado en GitHub Registry
-
Pruebas
Se provee una carpeta
examples
con ejemplos de programasegg
`Se ha automatizado el proceso de pasar del “ejemplo que funciona” a “test unitario que prueba que funciona”
Se hace integración contínua tanto en desarrollo como en producción
-
Documentación
Ejecutables, Lenguaje, ASTs, etc.
Documentación del módulo npm (API) y ejecutables
Opcional: publicar la documentación de la API usando GitHub Pages en la carpeta
docs/
. Informe de cubrimiento