@sofidfr/while v1.0.0
Lab Add Loops and Strings
- Author: Sofía De Fuentes Rosella
- Email: alu0101480619@ull.edu.es
Introduction
Using the grammar built from the previous labs, we are now expanding the functionality of our program by adding functions and the boolean type.
Opciones en línea de comandos
program
.version(version)
.argument("<filename>", 'file with the original code')
.option("-o, --output <filename>", "file in which to write the output")
.option("-V, --version", "output the version number")
.action((filename, options) => {
transpile(filename, options.output);
});
Se utiliza el paquete 'commander' para crear una interfaz por línea de comandos en una aplicación Node.js.
-V
: La versión se establece en el package.json-o
: Se indica el fichero en el que se imprime la salida
Análisis del programa
Lexer
%{
const reservedWords = ["fun", "true", "false", "i", "while", "for"]
const predefinedIds = ["print", "write" ]
function removeQuotes(s) {
return s.substring(1, s.length - 1);
}
const idOrReserved = text => {
if (reservedWords.find(w => w == text)) return text.toUpperCase();
if (predefinedIds.find(w => w == text)) return 'PID';
return 'ID';
}
%}
number [0-9]+(\.[0-9]+)?([eE][+-]?[0-9]+)?"i"?|"i"
string \"(?:[^"\\]|\\.)*\"
%%
\s+ /* skip whites */;
"#".* /* skip comments */;
\/\*(.|\n)*?\*\/ /* skip comments */;
{number} return 'N';
{string} { yytext = removeQuotes(yytext); return 'STRING'; }
[a-zA-Z_]\w* return idOrReserved(yytext); // must be after number
'**' return '**';
'==' return '==';
'&&' return '&&';
'||' return '||';
[-=+*/!(),<>@&{}\[\];] return yytext;
Este analizador léxico simplemente aplica reglas de reconocimiento de tokens para saber qué carácteres son aceptados por la gramática. Las reglas vienen definidas como expresiones regulares.
reservedWords
Contiene la lista de palabras reservadas del lenguaje. Contienen identificadores especiales y no pueden usarse como identificadores comunespredefinedIds
Contiene la lista de identificadores predefinidos que representan functiones o variables especiales ya existentes en el entornoremoveQuotes
Elimina las doble comillas que redean las stringsidOrReserved
Función que verifica si un texto es una palabra reservada, un identificador predefinido o un identificador común, y devuelve una clasificación adecuada
Gramática
Permite realizar diversas operaciones matemáticas sobre números enteros, flotantes u complejos. Además permite la asignación, el uso de la coma y PID para imprimir.
%left ','
%right '='
%nonassoc '<' '>'
%left '&&' '||'
%nonassoc '=='
%left '@'
%left '&'
%left '-' '+'
%left '*' '/'
%nonassoc UMINUS
%right '**'
%left '!'
%%
es: e { return { ast: buildRoot($e) }; }
;
e:
e ',' e { $$ = buildSequenceExpression([$e1, $e2]) }
| ID '=' e { $$ = buildAssignmentExpression($($ID), '=', $e); }
| e '==' e { $$ = buildCallMemberExpression($e1, 'equals', [$e2]); }
| e '<' e { $$ = buildCallMemberExpression($e1, 'lessThan', [$e2]); }
| e '>' e { $$ = buildCallMemberExpression($e1, 'greaterThan', [$e2]); }
| e '&&' e { $$ = buildLogicalExpression($e1, '&&', $e2); }
| e '||' e { $$ = buildLogicalExpression($e1, '||', $e2); }
| '!' e { $$ = buildUnaryExpression('!', $e); }
| e '@' e { $$ = buildMax($e1, $e2, true); }
| e '&' e { $$ = buildMin($e1, $e2, true); }
| e '-' e { $$ = buildCallMemberExpression($e1, 'sub', [$e2]); }
| e '+' e { $$ = buildCallMemberExpression($e1, 'add', [$e2]); }
| e '*' e { $$ = buildCallMemberExpression($e1, 'mul', [$e2]); }
| e '/' e { $$ = buildCallMemberExpression($e1, 'div', [$e2]); }
| e '**' e { $$ = buildCallMemberExpression($e1, 'pow', [$e2]); }
| '(' e ')' apply { $$ = buildParOrCallExpression($e, $apply); }
| '-' e %prec UMINUS { $$ = buildCallMemberExpression($e, 'neg', []); }
| e '!' { $$ = buildCallExpression('factorial', [$e], true); }
| N { $$ = buildCallExpression('Complex',[buildLiteral($N)], true); }
| TRUE { $$ = buildLiteral(true); }
| FALSE { $$ = buildLiteral(false); }
| STRING { $$ = buildLiteral($STRING); }
| WHILE e '{' e '}' { $$ = buildWhileExpression($e1, $e2); }
| FOR '(' e ';' e ';' e ')' '{' e '}' { $$ = buildForExpression($e1, $e2, $e3, $e4); }
| PID '(' eList ')' { $$ = buildCallExpression($PID, $eList, true); }
| ID apply { $$ = buildParOrCallExpression(buildIdentifier($($ID)), $apply); }
| FUN '(' idOrEmpty ')' '{' e '}'
{ $$ = buildFunctionExpression($idOrEmpty, $e); }
;
Operaciones soportadas
- Operaciones Binarias:
+
,-
,*
,/
,@
(max),&
(min),**
(potencia) - Operaciones Unarias:
-
(negativo),!
(factorial) - Números Complejos: Representados por
N
y tratados conComplex
(más info en apartado de Complejos) - Asignación: Permite asignar el resultado de una expresión a un identificador (variable) usando
=
- Operadores lógicos:
&&
(and),||
(or) - Operaciones de comparación:
==
,<
- Uso de la coma:
,
se usa para separar expresiones, permitiendo evaluar múltiples expresiones en secuencia - PID: permite imprimir el id usando la función
print
owrite
Precedencia de Operadores
- Coma:
','
- Asociatividad:
%left
(Izquierda) - Descripción: Utilizada para evaluar múltiples expresiones en secuencia, devolviendo el resultado de la última. Suele tener una precedencia baja, facilitando la ejecución de múltiples acciones en una sola expresión
- Asociatividad:
- Asignación:
'='
- Asociatividad:
%right
(Derecha) - Descripción: Generalmente tiene una de las precedencias más bajas, permitiendo cadenas de asignaciones
- Asociatividad:
- Operaciones lógicas y comparación:
'==' '<' '&&' '||'
- Asociatividad:
%left
(Izquierda) - Descripción: Permiten evaluar igualdades, comparaciones y operaciones lógicas entre expresiones. Se evalúan después de las operaciones aritméticas, pero antes de la igualdad y la coma
- Asociatividad:
- Máximo y mínimo:
'@' '&'
- Asociatividad:
%left
(Izquierda) - Descripción: Se evalúan después de todas las operaciones de mayor precedencia, agrupándose de izquierda a derecha en expresiones sucesivas sin paréntesis
- Asociatividad:
- Suma y resta:
'+' '-'
- Asociatividad:
%left
(Izquierda) - Descripción: Tienen mayor precedencia que el máximo y mínimo, pero menor que la multiplicación y división, evaluándose de izquierda a derecha
- Asociatividad:
- Multiplicación y división:
'*' '/'
- Asociatividad:
%left
(Izquierda) - Descripción: Una de las precedencias más altas, con evaluación de derecha a izquierda en expresiones con múltiples operadores de potencia
- Asociatividad:
- Potencia:
'**'
- Asociatividad:
%right
(Derecha) - Descripción: Una de las precedencias más altas, con evaluación de derecha a izquierda en expresiones con múltiples operadores de potencia
- Asociatividad:
- Factorial y negativo unario:
'!' 'UMINUS'
- Asociatividad:
%nonassoc
(No asociativa) - Descripción: La mayor precedencia, evaluándose antes de cualquier otro operador. La no asociatividad previene ambigüedades en expresiones directamente consecutivas sin paréntesis claros
- Asociatividad:
apply:
/* empty */ { $$ = []; }
| '(' ')' apply { $$ = [ null ].concat($apply); }
| '(' e ')' apply { $$ = [$e].concat($apply); }
;
empty
: Retorna un arreglo vacío. No hay argumentos de función o aplicación'(' ')'
: Maneja llamadas a funciones sin argumentos. Agreganull
a un array que se concatena con más contenidos de apply.'(' e ')'
: Para llamadas a funciones con argumentos
idOrEmpty:
/* empty */ { $$ = []; }
| ID { $$ = [ buildIdentifier($($ID)) ]; }
;
empty
: Representa la ausencia de un id. Retorna un array vacíoID
: Cuando se encuentra un id, construye un nodo AST para ese id y lo mete en un array
eList: { $$ = []; }
| eList ';' e { $$ = $eList.concat([$e]); }
| e { $$ = [$e]; }
;
empty
: Indica una lista de expresiones vacías. Retorna un array vacíoeList ';' e
: Permite construir listas de expresiones separadas por;
. Agrega la expresión aeList
. Esto es recursivo y permite listas de cualquier longitude
: Inicia una lista de expresiones con un único elemento.
Funciones
buildRoot
: Construye el nodo raíz del AST.buildBinaryExpression
: Construye nodos para expresiones binarias (e.g., suma, resta).buildLiteral
: Construye nodos para literales numéricos.buildCallExpression
: Construye nodos para llamadas a funciones, utilizado aquí para operaciones de factorial y potencia.buildUnaryExpression
: Construye nodos para expresiones unarias, como el negativo.buildIdentifier
: Crea nodos para identificadores, que representan nombres de variables o funciones en el códigobuildAssignmentExpression
: Construye nodos para expresiones de asignación, donde se asigna el resultado de una expresión a un identificadorbuildSequenceExpression
: Genera nodos para secuencias de expresiones, permitiendo representar múltiples expresiones evaluadas en secuenciabuildCallMemberExpression
: Construye nodos para llamadas a métodos sobre objetos, útil para operaciones con números complejos donde se invoca un método de un objetobuildMemberExpression
: Crea nodos para expresiones de miembro, usadas para acceder a propiedades o métodos de objetosbuildVariableDeclaration
: Genera nodos para declaraciones de variables, introduciendo nuevas variablesbuildVariableDeclarator
: Crea nodos para especificar variables individuales dentro de una declaraciónbuildMax
: Construye un nodo del AST para representar la llamada a la funciónMath.max
que devuelve el mayor entre dos númerosbuildMin
: Construye un nodo del AST para representar la llamada a la funciónMath.min
que devuelve el menor entre dos númerosbuildMethodExpression
: Construye nodos para llamadas a métodos sobre objetos, específicamente para operaciones con númerosbuildFunctionExpression
: Crea un nodo del AST con parámetros y cuerpo de función que contiene una instrucción de retornobuildIdCalls
: Construye un nodo del AST, aplicando una serie de llamadas sucesivas a un id inicial, cada una con sus propios argumentosbuildWhileExpression
: Construye un nodo para la expresión de buclewhile
dentro de una funciónbuildForExpression
: Construye un nodo para la expresión de buclefor
dentro de una funciónbuildArrowFunctionExpression
: Construye un nodo para una expresión de función de flecha.buildParOrCallExpression
: Construye un nodo para expresiones que pueden ser simples o secuencias de llamadas a funciones
Traducción de expresiones
Cuando se invoca calc2js.mjs
, se llama a la función transpile
con el nombre del archivo de entrada que contendrá algo así:
a = 0,
b = while a < 10 {
print(a),
a = a +1
},
print(b) # 10
La función transpile
se encarga de:
- Leer el contenido del archivo de entrada
- Parsear el código fuente para construir un AST
- Generar el código JS transpilado a partir del AST modificado, incluyéndo un preámbulo que
importa las dependencias necesarias desde una biblioteca de soporte
support-lib.js
- Escribe el código JS generado en el archivo de salida o por pantalla
La función que se encarga de traducir el código a JavaScript es codeGen(ast)
module.exports = function codeGen(ast) {
let fullPath = path.join(__dirname, 'support-lib.js');
let dependencies = Array.from(ast.dependencies).join(", ");
let preamble = template(dependencies, fullPath);
let output = preamble + recast.print(ast.ast).code;
return output;
}
- Primero utiliza el módulo
path
para obtener la ruta completa al archivosupport-lib.js
que contiene las implementaciones de las dependencias - La variable
dependencies
recoge todas las dependencias identificadas durante el análisis del AST. Estas se pasan atemplate
para generar el preámbulo del archivo de salida - Se utiliza
recast.print(ast.ast)
para convertir el AST modificado en código JS.recast
permite la lectura y la generación de código, manteniendo tanto como pueda el estilo original - El preámbulo se concatena con el código JS, formando la salida (
output
) y es retornado
while
Para la traducción de expresiones while
, en la gramática se llama a buildWhileExpression
, árbol que se encuentra en ast-build.js
function buildWhileExpression(test, body) {
return {
type: "CallExpression",
callee: {
type: "ArrowFunctionExpression",
id: null,
params: [],
body: {
type: "BlockStatement",
body: [
{
type: "VariableDeclaration",
declarations: [
{
type: "VariableDeclarator",
id: {
type: "Identifier",
name: "result"
},
init: {
type: "Literal",
value: false,
raw: "false"
},
kind: "let"
}
],
kind: "let"
},
{
type: "WhileStatement",
test: test,
body: {
type: "BlockStatement",
body: [
{
type: "ExpressionStatement",
expression: buildAssignmentExpression("result", "=", body)
}
]
},
},
{
type: "ReturnStatement",
argument: {
type: "Identifier",
name: "result"
},
},
],
},
async: false,
generator: false,
id: null,
expression: false
},
arguments: [],
};
}
- Se declara la función anónima (función flecha sin parámetros y nombre) que retorna un valor
- Se inicializa
result
a false. Aquí se almacena el resultado que devuelve la función - Se construye el bucle
while
.test
es la condición de parada.buildAssignmentExpression
permite asignar el valor de la función aresult
- Se retorna el resultado
for
Para la traducción de expresiones for
, en la gramática se llama a buildForExpression
, árbol que se encuentra en ast-build.js
function buildForExpression(init, test, update, body) {
return {
type: "CallExpression",
callee: {
type: "ArrowFunctionExpression",
id: null,
params: [],
body: {
type: "BlockStatement",
body: [
{
type: "VariableDeclaration",
declarations: [
{
type: "VariableDeclarator",
id: {
type: "Identifier",
name: "result"
},
init: {
type: "Literal",
value: false,
raw: "false"
},
kind: "let"
}
],
kind: "let"
},
{
type: "ForStatement",
init: init,
test: test,
update: update,
body: {
type: "BlockStatement",
body: [
{
type: "ExpressionStatement",
expression: buildAssignmentExpression("result", "=", body)
}
]
},
},
{
type: "ReturnStatement",
argument: {
type: "Identifier",
name: "result"
},
},
],
},
async: false,
generator: false,
id: null,
expression: false
},
arguments: [],
};
}
- Se declara la función anónima (función flecha sin parámetros y nombre) que retorna un valor
- Se inicializa
result
a false. Aquí se almacena el resultado que devuelve la función - Se construye el bucle
for
.init
es la inicialización,test
es la condición de prueba yupdate
es la actualizaciónbuildAssignmentExpression
permite asignar el valor de la función aresult
- Se retorna el resultado
strings
Para la traducción de strings, se han seguido los siguientes pasos:
- Definir en el lexer la ER:
\"(?:[^"\\]|\\.)*\"
para capturar cadenas de carácteres encerradas entre comillas dobles - Cuando la ER captura algo, se llama a
removeQuiotes
para quitarle las comillas. Se retorna luego el tokenSTRING
- En la gramática se llama a
buildLiteral
, cuya propiedadraw
representa cómo aparece el literal en el código fuente
Simetría
Para lograr la simetría con operaciones de distintos tipos se ha hecho lo siguiente: Ejemplo bool con string y viceversa
bool op string
booleanHandler.string = function (op, other) {
if (op === 'add') {
return String(this) + other;
} else if (op === 'equals') {
return String(this) === other ? true : "false";
} else if (op === 'lessThan') {
return String(this).length < other.length ? true : "false";
} else if (op === 'greaterThan') {
return String(this).length > other.length ? true : "false";
} else if (op === 'greaterThanOrEquals') {
return String(this).length >= other.length ? true : "false";
} else if (op === 'lessThanOrEquals') {
return String(this).length <= other.length ? true : "false";
}
}
- Si el primer operando es
bool
, se llama abooleanHandler
y si el segundo esstring
, se le concatena.string
. - Se define exactamente lo que se quiere hacer con cada operador. En este caso se convierte el valor booleano en string y
si es una suma, se concatenan. Si es una operación de comparación, también se convierte el valor booleano en string y se comparan. Si la expresión retorna
true
, se retorna ese valor bool. Pero si retornafalse
, se devuelve como string. Eso es para evitar errores.
stringHandler.boolean = function (op, other) {
if (op === 'add') {
return this + String(other);
} else if (op === 'equals') {
return (this == String(other)) ? true : "false";
} else if (op === 'lessThan') {
return (this.length < String(other).length) ? true : "false";
} else if (op === 'greaterThan') {
return (this.length > String(other).length) ? true : "false";
} else if (op === 'greaterThanOrEquals') {
return (this.length >= String(other).length) ? true : "false";
} else if (op === 'lessThanOrEquals') {
return (this.length <= String(other).length) ? true : "false";
}
}
- Si el primer operando es
string
, se llama astringHandler
y si el segundo esbool
, se le concatena.boolean
. - Se hace exactamente igual que antes. Se transforma el valor bool en string y se ejecutan las operaciones.
for (let op in Operators) {
// Extending the boolean class to give error messages for all airthmetic operations
Boolean.prototype[op] = function (other) {
if (booleanHandler[typeof other]?.call(this, op, other) === "false") return false;
return booleanHandler[typeof other]?.call(this, op, other) || booleanHandler.default.call(this, op, other)
};
Function.prototype[op] = function (other) {
if (booleanHandler[typeof other]?.call(this, op, other) === "false") return false;
return functionHandler[typeof other]?.call(this, op, other) || functionHandler.default.call(this, op, other)
};
String.prototype[op] = function (other) {
if (booleanHandler[typeof other]?.call(this, op, other) === "false") return false;
return stringHandler[typeof other]?.call(this, op, other) || stringHandler.default.call(this, op, other)
}
}
- Este último fragmento es lo que permite las llamadas a los handlers anteriores.
- El
if
sirve para cuando retornamos el string"false"
. Si retornara false como bool, la condición del or no se compliría y saliaría a la siguiente parte del or (esto daría problemas)
Mensajes de error para operaciones de distinto tipo no soportadas
La segunda parte del or anterior es lo que permite esto. El default handler
.
stringHandler.default = function (op, other) {
throw new Error(`String "${this}" does not support "${Operators[op] || op}" for "${other}"`)
}
Cuando la primera expresión del or
no se cumple, salta a la segunda, que contiene este mensaje de error.
Tests
Antes de ejecutar los tests se deben completar una serie de pasos:
- Se imprime la salida del programa en un archivo de salida:
bin/calc2js.mjs test/data/test4.calc -o test/data/correct4.js
- Se ejecuta con node y se comprueba que la salida es correcta:
node test/data/correct4.js
- Se imprime la salida anterior en el fichero de salida:
node test/data/correct4.js > test/data/correct-out4.txt
- Se añade a
test-description.mjs
el archivo:
{
input: 'test4.calc',
output: 'out4.js',
expected: 'correct4.js',
correctOut: 'correct-out4.txt'
},
- Se ejecuta el test:
npx mocha --grep 'test4'
- Para ejecutarlos todos a la vez:
npm run test
Uso de la IA
Para la documentación se utilizó Github Copilot
y se realizó alguna consulta a ChatGPT
para mayor
entendimiento del funcionamiento del programa
Examples
References
- Repo ULL-ESIT-GRADOII-PL/esprima-pegjs-jsconfeu-talk
- crguezl/hello-jison
- Espree
- astexplorer.net demo
- idgrep.js
- Introducción a los Compiladores con Jison y Espree
1 month ago