Lexer Generator
Objetivos
Usando el repo de la asignación de esta tarea construya un paquete npm y
publíquelo como paquete privado en GitHub Registry con ámbito @ULL-ESIT-PL-2021
y con nombre el nombre de su repo lexgen-code-aluAtGitHub
Una parte de los conceptos y habilidades a ejercitar con esta práctica se explican en la sección Creating and publishing a node.js module en GitHub y en NPM.
El módulo deberá exportar una función que construye analizadores léxicos:
const buildLexer =require('@ULL-ESIT-PL-2021/lexgen-code-aluAtGitHub');
La función buildLexer
se llamará con un array de pares
const myTokens = [
[`nombreToken1`: /regexpToken1/],
[`nombreToken2`: /regexpToken2/],
...
]
que describe el léxico del lenguaje y retornará una función lexer
que es el analizador léxico:
const lexer = buildLexer(myTokens);
Se establecen las siguientes consideraciones semánticas:
- Si un token tiene de nombre
SPACE
sus matching serán ignorados y no se añadirán a la lista de tokens - El token
ERROR
es especial y es automáticamente retornado por el analizador léxico generadolexer
en el caso de que la entrada contenga un error
Este es un ejemplo mas concreto de como usar la librería:
const buildLexer = require('@ull-esit-pl-1920/p10-t2-lexgen-code-aluXXX');
const SPACE = /(?<SPACE>\s+|\/\/.*)/;
const RESERVEDWORD = /(?<RESERVEDWORD>\b(const|let)\b)/;
const ID = /(?<ID>\b([a-z_]\w*))\b/;
const STRING = /(?<STRING>"([^\\"]|\\.")*")/;
const OP = /(?<OP>[+*\/=-])/;
const myTokens = [
['SPACE', SPACE], ['RESERVEDWORD', RESERVEDWORD], ['ID', ID],
['STRING', STRING], ['OP', OP]
];
const lexer = buildLexer(myTokens);
cuando lexer
es llamada con una cadena de entrada retorna la secuencia de tokens de esa cadena conforme a la descripción léxica proveída:
str = 'const varName = "value"';
r = lexer(str);
let expected = [
{ type: 'RESERVEDWORD', value: 'const' },
{ type: 'ID', value: 'varName' },
{ type: 'OP', value: '=' },
{ type: 'STRING', value: '"value"' }
];
test(str, () => {
expect(r).toEqual(expected);
});
Cuando se encuentra una entrada errónea lexer
produce un token con nombre ERROR
:
str = ' // Entrada con errores\nlet x = 42*c';
r = lexer(str);
expected = [
{ type: 'RESERVEDWORD', value: 'let' },
{ type: 'ID', value: 'x' },
{ type: 'OP', value: '=' },
{ type: 'ERROR', value: '42*c' }
];
Esta entrada es errónea por cuanto no hemos definido el token para los números.
El token ERROR
es especial en cuanto con que casa con cualquier entrada errónea.
Véase también el último ejemplo con errores en la sección Pruebas
Prerequisitos
Tóme esta práctica con calma por cuanto me parece es complicada en el estado de conocimientos de RegExps en el que estamos. Nos ha faltado una clase para establecer las bases necesarias.
En este vídeo se introducen los conceptos de expresiones regulares que son necesarios para la realización de esta práctica. Especialmente
lastindex
en el minuto 19:30- El uso de la sticky flag
/y
a partir del minuto 30 - Construcción de analizador léxico minuto 33:45
En los primeros 25 minutos de este vídeo se explica como realizar la práctica:
- Analizadores Léxicos: 03:00
Estúdielos antes de seguir adelante
Sugerencias
Puede partir de este código en el que se combina el uso de sticky y los grupos con nombre
const str = 'const varName = "value"';
console.log(str);
const SPACE = /(?<SPACE>\s+)/;
const RESERVEDWORD = /(?<RESERVEDWORD>\b(const|let)\b)/;
const ID = /(?<ID>([a-z_]\w+))/;
const STRING = /(?<STRING>"([^\\"]|\\.")*")/;
const OP = /(?<OP>[+*\/=-])/;
const tokens = [
['SPACE', SPACE], ['RESERVEDWORD', RESERVEDWORD], ['ID', ID],
['STRING', STRING], ['OP', OP]
];
const tokenNames = tokens.map(t => t[0]);
const tokenRegs = tokens.map(t => t[1]);
const buildOrRegexp = (regexps) => {
const sources = regexps.map(r => r.source);
const union = sources.join('|');
// console.log(union);
return new RegExp(union, 'y');
};
const regexp = buildOrRegexp(tokenRegs);
const getToken = (m) => tokenNames.find(tn => typeof m[tn] !== 'undefined');
let match;
while (match = regexp.exec(str)) {
//console.log(match.groups);
let t = getToken(match.groups);
console.log(`Found token '${t}' with value '${match.groups[t]}'`);
}
escribiendo una función makeLexer
que recibe como argumentos un array tokens
como en el ejemplo y retorna una función que hace el análisis léxico
correspondiente a esos tokens.
Pruebas
Deberá añadir pruebas usando Jest. Amplíe este ejemplo:
[~/.../github-actions-learning/lexer-generator(master)]$ pwd -P
/Users/casiano/local/src/github-actions-learning/lexer-generator
[~/.../github-actions-learning/lexer-generator(master)]$ cat test.js
// If you want debugging output run it this way:
// DEBUG=1 npm test
const debug = process.env["DEBUG"];
const { inspect } = require('util');
const ins = (x) => { if (debug) console.log(inspect(x, {depth: null})) };
const buildLexer =require('./index');
const SPACE = /(?<SPACE>\s+|\/\/.*)/;
const RESERVEDWORD = /(?<RESERVEDWORD>\b(const|let)\b)/;
const ID = /(?<ID>\b([a-z_]\w*))\b/;
const STRING = /(?<STRING>"([^\\"]|\\.")*")/;
const OP = /(?<OP>[+*\/=-])/;
const myTokens = [
['SPACE', SPACE], ['RESERVEDWORD', RESERVEDWORD], ['ID', ID],
['STRING', STRING], ['OP', OP]
];
let str, lexer, r;
lexer = buildLexer(myTokens);
str = 'const varName = "value"';
ins(str);
r = lexer(str);
ins(r);
let expected = [
{ type: 'RESERVEDWORD', value: 'const' },
{ type: 'ID', value: 'varName' },
{ type: 'OP', value: '=' },
{ type: 'STRING', value: '"value"' }
];
test(str, () => {
expect(r).toEqual(expected);
});
str = 'let x = a + \nb';
ins(str);
r = lexer(str);
expected = [
{ type: 'RESERVEDWORD', value: 'let' },
{ type: 'ID', value: 'x' },
{ type: 'OP', value: '=' },
{ type: 'ID', value: 'a' },
{ type: 'OP', value: '+' },
{ type: 'ID', value: 'b' }
];
ins(r);
test(str, () => {
expect(r).toEqual(expected);
});
str = ' // Entrada con errores\nlet x = 42*c';
ins(str);
r = lexer(str);
ins(r);
expected = [
{ type: 'RESERVEDWORD', value: 'let' },
{ type: 'ID', value: 'x' },
{ type: 'OP', value: '=' },
{ type: 'ERROR', value: '42*c' }
];
test(str, () => {
expect(r).toEqual(expected);
});
Ejemplo de ejecución:
[~/.../github-actions-learning/lexer-generator(master)]$ npm test
> @ULL-ESIT-PL-2021/lexer-generator@1.0.0 test /Users/casiano/local/src/github-actions-learning/lexer-generator
> jest 👈 use jest!
PASS ./test.js
✓ const varName = "value" (4ms)
✓ let x = a +
b
✓ // Entrada con errores
let x = 42*c (1ms)
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 1.126s
Ran all test suites.
Integración Contínua usando GitHub Actions
Use GitHub Actions para la ejecución de las pruebas
Documentación
Documente
el módulo incorporando un README.md
y la documentación de la función exportada.
Publicar como paquete npm en GitHub Registry
Usando el repo de la asignación de esta tarea publique el paquete como paquete privado en GitHub Registry con ámbito @ULL-ESIT-PL-2021
y nombre el nombre de su repo lexgen-code-aluAtGitHub
Pruebas de Producción
En un nuevo repo lexgen-testing-aluGitHub
añada las pruebas
para comprobar que el paquete publicado
se instala y puede ser usado correctamente.
Usando git submodule
configure un super-repo para que contenga
a ambos repos: el del módulo lexgen-code-aluAtGitHub
y el repo de pruebas de producción lexgen-testing-aluGitHub
.
Semantic Versioning
-
Publique una mejora en la funcionalidad del módulo. Por ejemplo añada la opción
/u
a la expresión regular creada para que Unicode sea soportado. ¿Como debe cambiar el nº de versión? -
Opcional: Un defecto que tiene el diseño del módulo es que el nombre de la expresión regular que define el token aparece dos veces: dentro de la regexp y en el array y debe ser la misma. Cambie la interfaz para que sólo aparezca una vez. ¿Como debe cambiar el nº de versión?
Referencias
- Tema Expresiones Regulares y Análisis Léxico
- Sección Creating and publishing a node.js module en GitHub y en NPM
- Jest
- Sección GitHub Registry
- Sección GitHub Actions
- Sección Módulos
- Sección Node.js Packages
- Sección Documentation
Rúbrica
Incidencias para el Project Board para la prácticaLexer Generator
El paquete está publicado en GitHub Registry
Opcional: La función exportada se llama con un array de pares
[[NAME, /(?<NAME>, ...)/] ... ]
en la que el nombre del token aparece repetido dos veces. Modifique la interfaz para que reciba sólo un array de expresiones regulares con nombre[/(?<NAME> ...)/, ... ]
El módulo exporta las funciones adecuadas
Contiene suficientes tests
Opcional: estudio de covering
Se ha hecho CI con GitHub Actions
Los informes están bien presentados
La documentación es completa
Opcional: publicar la documentación de la API usando GitHub Pages en la carpeta
docs/
Las pruebas de producción funcionan bien
El superproyecto está correctamente estructurado usando submódulos
Se ha hecho un buen uso del versionado semántico en la evolución del módulo
Opcional: se proporciona información de localización (offset, etc.)
Manejo de errores y blancos
Calidad del código