Egg. Make evaluate a method of nodes

Metodologia

  1. Trabaje partiendo de la práctica anterior. Puede usar la misma working copy.
  2. 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.
  3. 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.
  4. 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

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 switchque 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:

  1. bin/eggc prog.egg produce como salida un JSON prog.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.
  2. Cuando ejecutamos bin/eggvm prog.egg.evm falla porque la estructura del JSON es un mapa y ahora evaluate es un método definido en las clases de nodos VALUE, WORDy APPLY

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

Referencias

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áctica

Egg. 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

    1. Las llaves {} funcionan como alias de los paréntesis

    2. Sticky

    3. Comentarios

    4. Localización

  • Pruebas

    1. Se usa mocking

    2. Se provee una carpeta examples con ejemplos de programas egg`

    3. Se ha automatizado el proceso de pasar del “ejemplo que funciona” a “test unitario que prueba que funciona

    4. Se hace integración contínua

  • Documentación

    1. Ejecutables, Lenguaje, ASTs, etc.

    2. Documentación del módulo npm (API) y ejecutables como se usan

    3. 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

    1. La publicación cumple los estándares de publicación de un módulo (CI, versionado, documentación, etc.)

  • El bucle REPL

    1. Evalúa correctamente y no se despista

    2. Detecta expresiones incompletas

    3. Colores