Egg. Make evaluate a method of nodes
Metodologia
- Trabaje partiendo de la práctica anterior. Puede usar la misma working copy.
- Añada como remoto el repo de GitHub dado por la asignación de esta tarea. Quizá el nombre del remoto podría ser el nombre de la práctica.
- Haga también una branch con el nombre de cada práctica y manténgala actualizada hasta el último push de entrega de esa práctica.
- Mantenga los nombres de los ejemplos que aparecen en las descripciones de las prácticas.
Design: Smells, The Switch Smell, The Open Closed Principle and the Strategy Pattern
Lea esta sección:
procurando entender los principios SOLID, el problema del Switch Smell y el Strategy Pattern. Vea el vídeo de Elijah Manor.
Ejemplos de Jerarquía de Ficheros y Organización
- Jerarquía de Ficheros y Organización Ejemplo-1
- Jerarquía de Ficheros y Organización Ejemplo-2
- Jerarquía de Ficheros y Organización Ejemplo-3
Strategy Pattern en Egg
El diseño de Marijn Haverbeke sigue el strategy pattern y evita el switch smell mediante el uso de los hashes specialForms
, topEnv
, etc.
Para dar soporte a la idea, en nuestro módulo existirá algún módulo, llamémoslo public.js
que exporte los hashes specialForms
, topEnv
, etc. que faciliten la escritura de plugins que extiendan el lenguaje. Algo así:
➜ egg-4-alu0100966589 git:(master) ✗ cat lib/public.js
const egg = require('./eggvm');
const specialForms = require('./specialForms');
const astNodes = require('./ast_nodes');
module.exports = {
egg,
specialForms,
astNodes
};
Así un desarrollador externo que quiera extender nuestro Egg con un módulo egg-plugin-XXX
podría hacerlo. Para ello importará registry.js
del módulo principal e insertará en specialForms
y topEnv
sus extensiones.
Este sería un ejemplo de un plugin externo:
➜ egg-4-alu0100966589 git:(master) ✗ cat plugins/require.js
const fs = require('fs');
const { egg, specialForms } = require('../lib/public');
Primero obtiene egg
y specialForms
del módulo principal los (en este caso está en '../lib/public'
) y luego extiende topEnv
:
const requireResults = new Map();
egg.topEnv["require"] = (path) => {
if (typeof path !== 'string') {
throw new Error('invalid argument for require, expected a string');
}
if (requireResults.has(path)) {
return requireResults.get(path);
}
else {
const result = egg.runFromFile(path);
requireResults.set(path, result);
return result;
}
}
Esto permitiría que el futuro el intérprete cargara dinámicamente plugins que están en módulos separados. Algo así:
➜ egg-4-alu0100966589 git:(master) ✗ cat examples/require.egg
do(
def(str, require("examples/a.egg")),
print(str)
)
➜ egg-4-alu0100966589 git:(master) ✗ cat examples/a.egg
print("\thello\nworld\u2764")
Ahora podríamos ejecutar el intérprete dotándolo con una opción -p
que permita cargar plugins instalados:
➜ egg-4-alu0100966589 git:(master) ✗ bin/egg.js -p ../plugins/require.js -r examples/require.egg
hello
world❤
hello
world❤
AST con Clases: evaluate como método
Desafortunadamente es mucho mas difícil hacer un analizador sintáctico que cumpla el principio Open Closed y mas aún usando un analizador PDR.
Podemos sin embargo intentar mejorar un poco el código de la Egg virtual machine eliminando el switch
que actualmente existe en evaluate
en eggvm.js:
function evaluate(expr, env) {
switch(expr.type) {
case 'value':
return expr.value;
case 'word':
if (expr.name in env) {
return env[expr.name];
} else {
throw new ReferenceError(`Undefined variable: ${expr.name}`);
}
case 'apply':
if (expr.operator.type == 'word' && expr.operator.name in specialForms) {
return specialForms[expr.operator.name](expr.args, env);
}
let op = evaluate(expr.operator, env);
if (typeof op != "function") {
throw new TypeError('Applying a non-function');
}
return op(...expr.args.map((arg) => evaluate(arg, env)));
}
}
de manera que si en el futuro introducimos nuevos tipos de nodos en el AST la extensión para ese nuevo tipo de nodo (por ejemplo nodos methodcall
) sea mas modular, añadiendo simplemente un módulo (methodcall.js
) conteniendo una nueva clase (MethodCall
) en la que se exporta el correspondiente método evaluate
para ese tipo de nodos.
Modifique el AST para dar una solución OOP con clases:
- una clase
Value
- una clase
Word
- una clase
Apply
de manera que cada clase de objeto dispone de un método evaluate
.
[~/ull-pl1718-campus-virtual/tema3-analisis-sintactico/src/egg/crguezl-egg(private)]$ cat lib/ast.js
// The AST classes
const {specialForms} = require("./registry.js");
class Value {
constructor(token) {
...
}
evaluate() {
...
}
}
class Word {
constructor(token) {
...
}
evaluate(env) {
...
}
}
class Apply {
constructor(tree) {
...
}
evaluate(env) {
...
}
}
module.exports = {Value, Word, Apply};
Por supuesto, ahora, cuando el parser detecta un nuevo nodo en su construcción del árbol, crea un objeto de la clase correspondiente:
parseExpression() {
let expr;
if (this.lookahead.type === "STRING") {
expr = new Value(this.lookahead);
} else if (this.lookahead.type === "NUMBER") {
...
} else if (this.lookahead.type === "WORD") {
expr = new Word(this.lookahead);
} else {
throw ...
}
return this.parseApply(expr);
}
Aisle estas clases en un fichero lib/ast.js
.
La función evaluate
con el switch
que estaba inicialmente en lib/eggvm.js
desaparece en esta versión
Una Solución:
Actualice la máquina virtual evm
para que pueda ejecutar los JSON
Despúes de que hayamos definido las clases de nodos del AST y hayamos añadido evaluate
como método
en las clases creadas nos encontramos con que bin/eggvm
deja de funcionar. Esto es así porque:
bin/eggc prog.egg
produce como salida un JSONprog.egg.evm
conteniendo el mapa/hash del árbol descrita en JSON. En JSON no se puede describir que un objeto pertenece a una cierta clase. Ni siquiera existe el concepto de clase.- Cuando ejecutamos
bin/eggvm prog.egg.evm
falla porque la estructura del JSON es un mapa y ahoraevaluate
es un método definido en las clases de nodosVALUE
,WORD
yAPPLY
Solución
Escriba una función json2AST
que convierta la estructura de datos
plana en un AST en los que cada nodo pertenece a la clase correspondiente.
Modifique la función runFromEVM
que ejecuta el código de la máquina virtual para que siga funcionando. Algo como esto:
function runFromEVM(fileName) {
try {
let json = fs.readFileSync(fileName, 'utf8');
let treeFlat = JSON.parse(json);
let tree = json2AST(treeFlat);
let env = Object.create(topEnv);
return tree.evaluate(env);
}
catch (err) {
console.log(err);
}
}
Una Solucion: (repo privado)
Recursos
- El lenguaje egg: repo en GitHub
- Repo interpreter-egg
- NodeJS Readline gist
- En el repo ULL-ESIT-PL-1617/interpreter-egg se muestra como hacer un bucle REPL
- XRegExp
- El módulo @ull-esit-pl/example2test
Referencias
- Eloquent JS: Chapter 11. Project: A Programming Language
- Apuntes del curso 15/16: Code Smells/Código Hediondo
- Apuntes del curso 16/17: Patrones de Diseño
- Apuntes del curso 15/16: Eliminando Switch Smell
- Apuntes del curso 16/17: Strategy Pattern
- Apuntes del curso 16/17: Práctica: Evaluar Strategy Pattern
- Apuntes del curso 16/17: Práctica: Creación de Paquetes NPM y Strategy Pattern
- JSHint
Recursos del Profesor
Debugging Simple Examples
- Repo ULL-ESIT-PL-1819/private-egg
-
Paths:
[~/.../egg/crguezl-egg(json2ast)]$ pwd -P /Users/casiano/local/src/javascript/PLgrado/eloquentjsegg
-
Remotes:
[~/.../egg/crguezl-egg(json2ast)]$ git remote -v gist git@gist.github.com:2ec9aeb4e3fa512eec26.git (fetch) pl1617 git@github.com:ULL-ESIT-PL-1617/egg.git (fetch) pl1819 git@github.com:ULL-ESIT-PL-1819/egg.git (fetch) private-egg git@github.com:ULL-ESIT-PL-1718/egg.git (fetch) private-egg-1819 git@github.com:ULL-ESIT-PL-1819/private-egg.git (fetch)
- Repo TFA-davafons
/Volumes/2020/pl/pl1819/practicas/TFA-04-16-2020-03-22-00/davafons
- json2AST.js
- Repo p6-t3-egg-1-davafons
/Volumes/2020/pl/pl1819/practicas/p6-t3-egg-1-04-16-2020-03-13-25/davafons
Rúbrica
Incidencias para el Project Board para la prácticaEgg. Make evaluate a method of nodes
Metodología de trabajo y Jerarquía de ficheros
Se han añadido clases para los distintos tipos de nodos siguiendo el Strategy Pattern
Se dispone de un mecanismo para convertir los JSON en objetos de las clases del AST y el intérprete evm funciona
Alias de las palabras reservadas como
set/= define/def/:=
etc.-
Analizador Léxico
Las llaves
{}
funcionan como alias de los paréntesisSticky
Comentarios
Localización
-
Pruebas
Se usa mocking
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
-
Documentación
Ejecutables, Lenguaje, ASTs, etc.
Documentación del módulo npm (API) y ejecutables como se usan
Opcional: Documentación de la API de los módulos (parser, eggvm), informe de cubrimiento, etc.
set (asignación y manejo de ámbitos)
Librerías separadas (Parser, Intérprete, etc.)
-
Ejecutables (uno con opciones o varios ejecutables)
-
Se ha publicado en GitHub Registry
La publicación cumple los estándares de publicación de un módulo (CI, versionado, documentación, etc.)
-
El bucle REPL
Evalúa correctamente y no se despista
Detecta expresiones incompletas
Colores