| title | Práctica 3 GLF |
|---|---|
| author | Juan Horrillo Crespo |
El objetivo de esta práctica es implementar un pequeño intérprete de fórmulas inspirado en una hoja de cálculo. El programa carga primero un fichero con valores asociados a celdas y después entra en modo interactivo, donde acepta fórmulas introducidas por el usuario, las analiza y devuelve su resultado.
El lenguaje soporta referencias a celdas, números, operadores aritméticos y relacionales, rangos, llamadas a función y expresiones condicionales mediante IF. Para procesarlo se han utilizado Flex para el análisis léxico y Bison para el análisis sintáctico. A partir de la gramática se construye un árbol de sintaxis abstracta (AST), que permite separar claramente la comprobación semántica de la evaluación final.
Los objetivos principales son:
- Definir formalmente el lenguaje aceptado por el intérprete.
- Implementar el análisis léxico y sintáctico de las fórmulas.
- Construir un AST que represente la estructura de cada expresión.
- Validar errores léxicos, sintácticos y semánticos de forma controlada.
- Evaluar referencias, operaciones, rangos, funciones y condicionales.
- Documentar las decisiones de diseño, las limitaciones y los casos de prueba.
El proyecto se compila con make. Es necesario tener instalados gcc, make, flex, bison y la biblioteca de desarrollo de readline.
makeEl ejecutable generado se llama cell. Para iniciarlo hay que pasar como argumento un fichero de datos .cdf:
./cell data/hoja_1.cdfUna vez dentro del intérprete, las fórmulas deben comenzar por =. También se puede consultar la ayuda de una función con ? y salir con EXIT.
>>> =A1+A2*2
[1] 32.000000
>>> ?SUM
>>> EXIT
Para eliminar los artefactos generados por Flex/Bison en el directorio build y el ejecutable cell:
make cleanEn esta sección del documento se definirá formalmente el lenguaje de operaciones sobre hojas de calculo descrito en el enunciado como una gramática independiente de contexto.
El alfabeto está formado por los subalfabetos
-
Letras mayúsculas:
$\Sigma_L={A, B, C, D, \dots, Y, Z}$ -
Dígitos decimales:
$\Sigma_Z={0, 1, 2, 3, \dots, 8, 9}$ -
Operadores aritméticos:
$\Sigma_A={+,-,*,/}$ -
Operadores relacionales:
$\Sigma_R={<,>,=,\leq,\geq,<>}$ -
Paréntesis:
$\Sigma_P={;(;, ;);}$ -
Separadores:
$\Sigma_S={;.;,;\text{``,''};,;:;}$ -
Interpretación de sentencia:
$\Sigma_I={=, ?}$
Por tanto
- Inicio de sentencia:
Con un = delante para indicar que se debe evaluar la sentencia, un ? para mostrar la ayuda de una función o EXIT para salir del programa. El comando EXIT se procesa directamente en el bucle interactivo, antes de llamar al analizador léxico y sintáctico.
S ==> =<Expr>|?<Id>
- Expresiones y recursividad:
Para poder definir correctamente operaciones complejas, modelizo mediante las produccciones la prioridad de operadores (relacionales, suma o resta y multiplicación o división en ese orden)
<Expr> ==> <Expr><OpRel><Arit>|<Arit>
<Arit> ==> <Arit><OpAdd><Term>|<Term>
<Factor> ==> <Factor><OpMul><Term>|<Term>
<Term> ==> (<Expr>)|-<Term>|<Func>|<Rango>|<Celda>|<Valor>
<OpRel> ==> <|>|=|<=|>=|<>
<OpAdd> ==> +|-
<OpMul> ==> *|/
- Función e identificadores:
Define de que elementos se compone una función y cuales son las funciones aceptadas (por su identificador)
<Func> ==> <Id>(<Arg>)
<Id> ==> <Letra>+
La validez del identificador de función se comprueba durante el análisis semántico. Sintácticamente se aceptan identificadores en mayúsculas, y después se resuelven contra las funciones implementadas (SUM, AVERAGE, MIN, MAX, COUNT, PRODUCT, ABS, FLOOR, CEIL, ROUND, IF).
- Argumentos, rango, celda y otros:
Todo lo que pueda ser un argumento de una función
<Arg> ==> <Expr>|<Expr>,<Arg>
<Rango> ==> <Celda>:<Celda>
<Celda> ==> <Columna><P>
<Columna> ==> <Letra>|<Letra><Columna>
<Letra> ==> cualquier c en Letras Mayúsculas
- Valor:
Basicamente cualquier número real bien formado, es decir, sin ceros por la izquierda en la parte entera y sin punto si no hay parte decimal. El signo negativo no forma parte del número: se trata como operador unario en la gramática.
<Valor> ==> <Numero>
<Numero> ==> <Natural>|<Natural>.<Decimal>
<Natural> ==> 0|<P>
<P> ==> <Digito+><C>
<C> ==> <Digito><C>|epsilon
<Digito+> ==> 1|2|3|···|8|9
<Digito> ==> 0|1|2|···|8|9
Muchas de estas producciones se pueden simplificar gracias al uso de expresiones regulares en el lexer, como la del número o las celdas. Se explicará en detalle en la especificación léxica del intérprete.
El fichero de datos que contiene los valores de las celdas será del formato presentado en el enunciado de la práctica, esto es un valor por línea y separado por espacios de la forma CELDA VALOR. El ejemplo que sue muestra es el siguiente:
A1 10
A2 20
A3 30
B1 4.6
B2 8
B3 12
C1 -2
C2 -5.15
equivalente a la hoja de cálculo
| A | B | C | |
|---|---|---|---|
| 1 | 10 | 4.6 | -2 |
| 2 | 20 | 8 | -5.15 |
| 3 | 30 | 12 |
Tal y como dice el enunciado, es importante especificar la política adoptada frente a errores en el fichero:
- Una línea mal formada es una línea que no sigue el formato
CELDA VALOR, separada obligatoriamente por un único caracter espacio, o que contenga más de una entrada. Ejemplos de error:A212,A2,12,A2,12,A2 12 A3 4. - La repetición de celdas será tratado como un error, es indispensable que solo aparezcan una vez en el fichero. Ejemplo de error:
A2 12
A2 20
- También es un error introducir un
VALORno numérico en las entradas. Ejemplos de error:A1 HOLA,A2 TRUE. - Será un error de referencia si la
CELDAno sigue los estandares (una columna dada por una o varias letras mayúsculas y un número positivo natural). Ejemplos de error:A 12,12 12,A0 12,A-1 12. - Además, será necesario que en cada línea presente del fichero haya una entrada, no podrá haber líneas vacias o formadas por caracteres de escape
\to\nentre otros y obligatoriamente mínimo una entrada, no se admiten ficheros de datos vacios. Ejemplo error:
A1 1\n
A2 2\n
- Para acotar el problema en cuanto a los tipos de datos, todo
VALORserá punto flotante de doble precisión (double). No se admite notación cientifica ni expresiones aritméticas, funciones, etc. Ejemplos de error:A3 IF(A2 > A1, 1, 0),A1 1e10,A1 2+1.
En caso de que ocurra un error de datos, antes de comenzar la sesión interactiva se mostará un error específico al usuario y terminará el programa.
Los mensajes de error son los siguentes:
- Error de malformación:
Error de sintaxis: Se esperaban 2 tokens <CELDA VALOR>, se encontraron 3 (A2 12 4). - Error de colisión (repetición):
Redefinición de celda: El identificador A2 ya fue inicializado. - Error de tipo:
Tipo de dato inválido: El valor 'TRUE' no pertenece al dominio de los números reales. - Referencia invalida:
Referencia a celda inválida: 'A0' no cumple el formato requerido. - Errores de fichero:
- Fichero vacío:
El archivo de entrada está vacío. - Línea vacía:
Línea vacía o compuesta exclusivamente caracteres de escape/control
- Fichero vacío:
Todos los edge cases se expondrán en detalle en la sección de casos de prueba
Para almacenar los datos en memoría usaré la estructura hash table, que permite realizar accesos por clave (la celda). Usaré la implementación de las famosas bibliotecas single header stb (enlace github).
El fichero de datos tendrá la extensión
.cdf(cell data file), pero cualquier documento de texto plano con el formato correcto podrá ser analizado.
El analizador léxico se implementa en grammar/cell.l utilizando Flex. A continuación se describen los tokens definidos, las expresiones regulares empleadas y las decisiones de diseño más relevantes.
| Token | Lexema(s) | Descripción |
|---|---|---|
T_FORMULA |
= |
Signo = que introduce una fórmula o actúa como operador relacional de igualdad |
T_HELP |
? |
Consulta de ayuda de una función |
T_CELL |
p.ej. A1, BC10, AA15 |
Referencia de celda; lleva el texto como valor semántico (yylval.str) |
T_NUM |
p.ej. 0, 25, 3.14 |
Número entero o real (sin signo); lleva el valor double en yylval.num |
T_OPR_SUM |
+ |
Operador aritmético suma |
T_OPR_RES |
- |
Operador aritmético resta / unario negativo |
T_OPR_MUL |
* |
Operador aritmético multiplicación |
T_OPR_DIV |
/ |
Operador aritmético división |
T_OPR_LESS |
< |
Operador relacional menor que |
T_OPR_GREATER |
> |
Operador relacional mayor que |
T_OPR_LEQ |
<= |
Operador relacional menor o igual |
T_OPR_GEQ |
>= |
Operador relacional mayor o igual |
T_OPR_NEQ |
<> |
Operador relacional distinto |
T_LPAR |
( |
Paréntesis de apertura |
T_RPAR |
) |
Paréntesis de cierre |
T_COMMA |
, |
Separador de argumentos |
T_COLON |
: |
Separador de rango |
T_FUNCTION |
SUM, AVERAGE, IF |
Identificador de función; lleva el texto como valor semántico (yylval.str) |
El lexema
=tiene dos roles distintos. Al comienzo de la entrada introduce la fórmula (S ==> T_FORMULA EXPR); dentro de una expresión actúa como operador relacional de igualdad (OP_REL ==> T_FORMULA). Esta distinción se resuelve sintácticamente.
LETRA → [A-Z]
DIGITO → [0-9]
DIGITO_NZ → [1-9]
CELDA → {LETRA}+{DIGITO_NZ}{DIGITO}*
NATURAL → 0 | {DIGITO_NZ}{DIGITO}*
NUMERO → {NATURAL}(\.{DIGITO}+)?
BLANCO → [ \t]+
La expresión CELDA captura referencias del tipo A1, BC10, AA15. Es importante que la columna numérica no puede comenzar por cero (por eso se hace uso de DIGITO_NZ), lo que impide referencias inválidas como A0.
El lexer no define un token distinto para cada función. Cualquier secuencia de letras mayúsculas ({LETRA}+) se reconoce como T_FUNCTION, y la resolución concreta se hace después, durante el análisis semántico.
Las referencias de celda se reconocen antes que los identificadores de función:
- Referencia de celda válida (
{CELDA}) - Referencia de celda mal formada (
{LETRA}+{DIGITO}+) - Número mal formado con ceros iniciales
- Número (
{NUMERO}) - Identificador de función (
{LETRA}+) - Operadores y separadores
Por ejemplo, A1 se reconoce como celda, mientras que SUM se reconoce como identificador de función. Cuando se encuentra con un identificador no implementado también llega al parser como un token T_FUNCTION. Estos identificadores se rechazan más tarde con un error semántico.
El signo - no se gestiona léxicamente como parte del número. El token T_NUM solo reconoce números sin signo. El signo negativo se trata sintácticamente como el operador unario T_OPR_RES aplicado a una expresión, lo que permite formas como -(A1+B1) o -SUM(A1:A3) sin ambigüedad.
Los blancos (espacio y tabulador) se ignoran. Como el sistema funciona como un intérprete interactivo (una entrada vacía no debería hacer nada) y por motivos de implementación (la función readline no incluye el salto de línea), he decidido que este carácter no forme parte del alfabeto.
Se definen tres tipos de errores léxicos: símbolos no reconocidos, referencias de celda mal formadas y números mal formados por ceros iniciales.
Los mensajes que devuelve el lexer son:
| Caso | Ejemplo | Mensaje |
|---|---|---|
| Referencia de celda mal formada | A0, A01 |
Error léxico: referencia de celda mal formada '<lexema>' |
| Número mal formado | 01, 00.5 |
Error léxico: número mal formado '<lexema>' |
| Símbolo no reconocido | @, #, letras minúsculas |
Error léxico: símbolo '<lexema>' no reconocido |
Para todos los casos se pone a true la bandera error_lexico y se devuelve el token especial YYerror, de forma que después Bison no muestre además un error sintáctico genérico para la misma entrada (de otra forma imprimiría Error: syntax error).
El analizador sintáctico se implementa en grammar/cell.y utilizando Bison. Con el se comprueba que la entrada sigue la forma del lenguaje de fórmulas de hojas de cálculo propuesta y al mismo tiempo construye el árbol de sintaxis abstracta (AST) que se analizará y evaluará después de forma semántica.
La gramática finalmente implementada en el parser es la siguiente:
S -> T_FORMULA EXPR
| T_HELP T_FUNCTION
EXPR -> EXPR OP_REL ARIT
| ARIT
ARIT -> ARIT OP_ADD FACT
| FACT
FACT -> FACT OP_MUL TERM
| TERM
TERM -> T_LPAR EXPR T_RPAR
| T_OPR_RES TERM
| FUNC
| RANGO
| T_CELL
| T_NUM
FUNC -> T_FUNCTION T_LPAR ARG T_RPAR
ARG -> ARG_ITEM
| ARG T_COMMA ARG_ITEM
ARG_ITEM -> EXPR
RANGO -> T_CELL T_COLON T_CELL
OP_REL -> T_OPR_LESS
| T_OPR_GREATER
| T_OPR_LEQ
| T_OPR_GEQ
| T_OPR_NEQ
| T_FORMULA
OP_ADD -> T_OPR_SUM
| T_OPR_RES
OP_MUL -> T_OPR_MUL
| T_OPR_DIV
Como ya se mencionó, el comando EXIT no aparece en la gramática porque se resuelve dentro del bucle interactivo principal antes de llamar a Bison. Así se evita mezclar "comandos del intérprete" con las fórmulas que debe reconocer el parser.
Srepresenta una entrada completa. Puede ser una fórmula introducida por=o una consulta de ayuda introducida por?.EXPRagrupa las expresiones relacionales. Aquí se reconocen comparaciones comoA1>A2,A1=A2oSUM(A1:A3)<>0.ARITagrupa sumas y restas.FACTagrupa multiplicaciones y divisiones.TERMrepresenta las unidades básicas de una expresión: paréntesis, negación, llamada a función, rangos, celda o simplemente un número.FUNCreconoce llamadas a función con una lista de argumentos entre paréntesis.ARGyARG_ITEMconstruyen listas de argumentos separadas por comas. Cada argumento puede ser cualquier expresión válida.RANGOreconoce la forma sintácticaCELDA:CELDA.OP_REL,OP_ADDyOP_MULtraducen tokens del lexer a operadores internos del AST.
Para resolver la precedencia no se hace con las directivas de Bison %left o %right, sino separando la gramática en varios niveles, que crean producciones correctas mediante recursividad:
EXPRreconoce operadores relacionales.ARITreconoce suma y resta.FACTreconoce multiplicación y división.TERMreconoce elementos básicos y expresiones entre paréntesis.
Con esta estructura, * y / tienen más prioridad que + y -, y las operaciones aritméticas tienen más prioridad que las comparaciones. Por ejemplo, =A1+A2*2 se interpreta como A1+(A2*2), mientras que =(A1+A2)*2 fuerza otra la otra agrupación por los paréntesis.
La recursión izquierda en las producciones ARIT -> ARIT OP_ADD FACT y FACT -> FACT OP_MUL TERM hace que los operadores sean asociativos por la izquierda. Así, A1-A2-A3 se interpreta como (A1-A2)-A3.
El operador - tiene dos usos: como resta binaria en OP_ADD y como negación en TERM -> T_OPR_RES TERM. Esta segunda producción permite expresiones como =-A1, =-(A1+A2) o =-SUM(A1:A3) que si hubiesemos tratado en la especificación léxica no hubieran sido posibles.
Si bien es cierto que usar un AST quizás se salga de los objetivos de la práctica y de la asignatura, buscando el cómo hacer análisis y evaluaciones semánticas me encontre con el blog de Ruslan Spivak, donde tenía una publicación sobre como construior un intérprete mediante un AST: Let's Build A Simple Interpreter, Part 7. Me pareció muy curioso y potente y decidí implementar el proyecto de la misma forma.
Durante el análisis sintáctico se construye un árbol de sintaxis abstracto. Cada producción de Bison devuelve un nodo o una lista de nodos que representan de forma abstracta la interpretación de la exprtesión. El resultado de una fórmula completa se guarda en la variable global ast_root para poder acceder a el posteriormente.
S -> T_FORMULA EXPR { ast_root = EXPR; }
El AST se construye con los constructores definidos para cada tipo de nodo:
| Producción | Nodo construido |
|---|---|
EXPR -> EXPR OP_REL ARIT |
new_binary_node(op, left, right) |
ARIT -> ARIT OP_ADD FACT |
new_binary_node(op, left, right) |
FACT -> FACT OP_MUL TERM |
new_binary_node(op, left, right) |
TERM -> T_OPR_RES TERM |
new_unary_node(UNARY_NEG, child) |
TERM -> FUNC |
Devuelve el nodo de función ya construido |
TERM -> RANGO |
Devuelve el nodo de rango ya construido |
TERM -> T_CELL |
new_cell_node(cell_reference) |
TERM -> T_NUM |
new_value_node(value) |
FUNC -> T_FUNCTION '(' ARG ')' |
new_function_node(name, arguments) |
RANGO -> T_CELL ':' T_CELL |
new_range_node(start, end) |
Las listas de argumentos se construyen con new_arguments() y add_argument(). Internamente son arrays dinámicos de punteros a nodos AST, lo que permite que una función reciba una mezcla de literales, celdas, subexpresiones, rangos y otras funciones.
La gramática se limita a comprobar la forma sintáctica. Algunas restricciones se dejan deliberadamente para el análisis semántico:
T_FUNCTIONacepta cualquier identificador formado por letras mayúsculas. Si la función existe o no se decide después.IFno tiene una producción especial. Se reconoce como una función normal y luego se comprueba que tenga tres argumentos y que el primero sea una comparación.RANGOacepta cualquier parCELDA:CELDA. Después se valida si el rango es horizontal o vertical, si está invertido, si es matricial o si contiene celdas sin valor.- Una celda sintácticamente correcta puede no existir en la tabla cargada; eso también se detecta semánticamente.
Esta separación evita convertir la gramática en una lista de casos especiales y mantiene el parser centrado en la estructura de la fórmula.
Para mostrar errores personalizados dependientes del contexto, se utiliza la "directiva" de configuración de Bison %define parse.error custom, lo que permite implementar la función yyreport_syntax_error. En esta función se usan distintas macros de Bison que permiten conocer el estado en el que se está, que token llevo a ese estado, el token que se acaba de leer (lookahead) o los tokens esperados por la producción. Mediante una combinación de todo eso, me permite tratar los siguentes casos de error:
| Caso | Ejemplo | Mensaje |
|---|---|---|
| Entrada sin símbolo inicial válido | SUM(A1:A3) |
Error de sintaxis: Consulta desconocida, se esperaba '=' o '?' |
| Función sin paréntesis de apertura | =SUM A1 |
Error de sintaxis: Se esperaba '(' tras la función |
| Dos expresiones contiguas | =SUM(A1 A2) |
Error de sintaxis: Falta un operador entre expresiones |
| Coma sin expresión antes o después | =SUM(,A1) |
Error de sintaxis: Coma mal situada, se esperaba una expresión antes o después de ',' |
| Coma antes de cerrar argumentos | =SUM(A1,) |
Error de sintaxis: Coma mal situada, no puede aparecer justo antes de ')' |
| Paréntesis desbalanceados | =SUM(A1:A3 |
Error de sintaxis: Paréntesis desbalanceados, se esperaba ')' |
| Rango incompleto o mal formado | =SUM(A1:) |
Error de sintaxis: Incorrecto formato de rango, se esperaba <CELDA:CELDA> |
| Expresión incompleta | =A1+ |
Error de sintaxis: Expresión incompleta, se esperaba un número, una celda, una función o '(' |
| Ayuda sin función | ? |
Error de sintaxis: Se esperaba un identificador de función para mostrar la ayuda |
| Otro error no clasificado | depende del contexto | Error de sintaxis: Error de sintaxis no específico |
Una vez que Bison ha aceptado la entrada, el programa ya no trabaja con texto sino con el AST construido durante el análisis sintáctico. Sobre ese árbol se hacen dos pasadas distintas:
- Análisis semántico, que comprueba que la fórmula tiene sentido con respecto a las reglas del lenguaje y a la tabla de celdas cargada.
- Evaluación, que calcula el valor final solo si el análisis semántico previo ha sido correcto.
Esta separación permite rechazar expresiones bien formadas sintácticamente pero inválidas en tiempo de ejecución, como =C1 (celda no definida), =ABS(A1:A3) o =SUM(A1:B2), antes de intentar calcular su resultado.
Se han contemplado los errores semánticos propuestos por el enunciado de la práctica y alguna extensión propuesta por mi.
-
Referencia a una celda inexistente Cada nodo
AST_CELLse valida consultando la tabla cargada desde el fichero de datos. Si una celda no existe o no tiene valor asignado, el análisis semántico falla. Esto afecta tanto a referencias directas (=C1) como a celdas incluidas dentro de un rango (=SUM(A1:A5)). -
División entre cero La división se comprueba durante la evaluación del nodo binario
BINARY_DIV, porque el divisor puede ser una expresión completa y no necesariamente un literal y no se sabe su valor hasta tiempo de ejecución. Por ejemplo,=A1/0se detecta como error, pero=A1/B3también siempre queB3sea0.0. -
Aridad incorrecta en funciones Las funciones unarias (
ABS,FLOOR,CEIL,ROUND) deben recibir exactamente un argumento. La funciónIFdebe recibir exactamente tres argumentos y además con restricciones: una operación relacional como condición condición, una expresión si es verdadera y una expresión si es falsa. Las funcionesSUM,AVERAGE,MIN,MAX,COUNT,PRODUCTaceptan un número variable de argumentos. -
Rango no válido Sintácticamente un rango es cualquier par
CELDA:CELDA, pero semánticamente se comprueba que pueda expandirse de forma válida. En la implementación se admiten rangos horizontales y verticales, pero no rangos matriciales comoA1:B2. También se rechazan rangos invertidos comoA3:A1y rangos que contienen alguna celda sin valor asignado. -
Uso de un rango donde no procede
Los rangos se han restringido a argumentos de funciones. Por tanto, una expresión como
=A1:A3es sintácticamente reconocible, pero semánticamente inválida. Además, las funciones unarias no aceptan rangos como argumento, porque operan sobre un único valor escalar.
La función analyze_tree() recorre recursivamente en postorden el AST antes de evaluar la expresión. Para cada tipo de nodo se aplica una regla:
AST_VALUE: siempre es válido.AST_CELL: debe existir en la tabla de datos.AST_UNARY: se analiza su hijo.AST_BINARY: se analizan sus dos operandos.AST_FUNCTION: primero se comprueba que la función exista y luego se delega enanalyze_function().AST_RANGE: solo es válido cuando se analiza desde una función que acepta rangos.
La validación de funciones se separa por familias. Para IF se comprueba su aridad y que el primer argumento sea una comparación. Para las funciones unarias se comprueba que haya un único argumento y que no sea un rango (no se puede hacer el valor absoluto de un rango). Las funciones con aridad indefinida recorren todos sus argumentos y, cuando encuentran un rango, llaman a analyze_range() para verificarlo (en estas si que se puede incluir un rango, pues en la evaluación se expanden como otros argumentos más).
Si el análisis semántico devuelve éxito, el programa llama a evaluate_tree(). La evaluación también recorre el AST:
- Los literales devuelven su valor
double. - Las celdas consultan su valor en la tabla cargada.
- El negativo unario cambia el signo del resultado de su hijo.
- Los nodos binarios calculan suma, resta, multiplicación, división o comparaciones.
- Las funciones se delegan a
evaluate_function().
Las comparaciones devuelven un valor lógico que en realidad es un número (para simplificar la cantidad de tipos en el parser). En evaluate_if(), la condición decide qué rama se evalúa: si es verdadera se evalúa el segundo argumento; si es falsa se evalúa el tercero (aprovecha que en C cualquier valor no nulo se interpreta como verdadero).
Los rangos se expanden dentro de las funciones agregadas. Por ejemplo, SUM(A1:A3,10,B1) suma los valores de A1, A2, A3, el literal 10 y el valor de B1. COUNT cuenta cada celda del rango como un elemento independiente (osea, su "tamaño").
Aquí se muestra otra vez la tabla con el error y el mensaje personalizado asociado, en este caso refiriendose a la semántica del lenguaje.
| Caso | Ejemplo | Mensaje |
|---|---|---|
| Celda inexistente o sin valor | =C1 |
Error semántico: La celda referenciada no tiene valor asignado |
| Función no definida | =UNKNOWN(A1) |
Error semántico: 'UNKNOWN' no es una función definida |
| Rango usado fuera de una función | =A1:A3 |
Error semántico: Los rangos solo se pueden usar como argumentos de funciones |
| División entre cero | =A1/0 |
Error semántico: División entre cero |
IF con aridad incorrecta |
=IF(A1>A2,100) |
Error semántico: La función 'IF' esperaba 3 argumentos |
IF sin comparación |
=IF(A1,100,200) |
Error semántico: La función 'IF' esperaba una comparación |
| Función unaria con aridad incorrecta | =ABS(A1,A2) |
Error semántico: La función 'ABS' esperaba un único argumento |
| Función unaria con rango | =ABS(A1:A3) |
Error semántico: La función 'ABS' no acepta rangos como argumento |
| Rango matricial | =SUM(A1:B2) |
Error semántico: No se admiten rangos matriciales |
| Rango invertido | =SUM(A3:A1) |
Error semántico: Inconsistencia en el acceso, mal orden |
| Ayuda de función no definida | ?UNKNOWN |
Error semántico: 'UNKNOWN' no es una función definida |
Todas las pruebas toman como datos de referencia el fichero hoja_1.cdf, de forma que los valores esperados sean reproducibles. El contenido del fichero es el siguiente:
A1 12
A2 10
A3 5
A4 -2
B1 4
B2 4
B3 0
Z99 1000
ZZZZ1 2000
El objetivo de estos casos es cubrir las partes principales del lenguaje descritas en el enunciado:
| Categoría | Casos cubiertos |
|---|---|
| Operaciones simples | Referencias a celdas, literales numéricos, suma, multiplicación, división y negativo unario. |
| Precedencia y paréntesis | Comparación entre =A1+A2*2 y =(A1+A2)*2. |
| Operadores relacionales | Comparaciones con >, <, = y <>. |
| Rangos | Rangos verticales (A1:A3), horizontales (A1:B1) y rangos inválidos. |
| Funciones de aridad variable | SUM, AVERAGE, MIN, MAX, COUNT y PRODUCT, mezclando rangos, celdas y literales. |
| Funciones unarias | ABS, FLOOR, CEIL y ROUND. |
| Condicionales | IF con condición verdadera, condición falsa y errores en la condición. |
| Errores sintácticos | Entrada sin =, coma mal situada, falta de operador, paréntesis desbalanceados, rango incompleto y ayuda incompleta. |
| Errores semánticos | División entre cero, celda inexistente, función no definida, aridad incorrecta, rango matricial, rango invertido y uso indebido de rangos. |
Los errores léxicos se comprueban con entradas independientes porque no dependen del fichero de datos. Los casos representativos son referencias de celda mal formadas (=A0, =A01), números con ceros iniciales (=01, =00.5) y símbolos no reconocidos (=@, =#).
En la siguente tabla se muestra la batería de casos de prueba propuesta:
| Expresión | Valor esperado | Prueba |
|---|---|---|
=A1 |
12.000000 |
Referencia simple a celda existente. |
=42 |
42.000000 |
Literal numérico. |
=A1+A2*2 |
32.000000 |
Precedencia de multiplicación sobre suma. |
=(A1+A2)*2 |
44.000000 |
Uso de paréntesis para alterar precedencia. |
=-A4 |
2.000000 |
Operador unario negativo. |
=A1/A2 |
1.200000 |
División correcta. |
=A1/0 |
Error semántico: división entre cero | Detección de división por cero. |
=A1/B3 |
Error semántico: división entre cero | Detección de división por cero. |
=A1>A2 |
1.000000 |
Comparación mayor que verdadera. |
=A1<A2 |
0.000000 |
Comparación menor que falsa. |
=A1=A2 |
0.000000 |
Comparación de igualdad. |
=A1<>A2 |
1.000000 |
Comparación de desigualdad. |
=SUM(A1:A3) |
27.000000 |
Suma de rango vertical. |
=SUM(A1:A3,1) |
28.000000 |
Función variádica mezclando rango y literal. |
=AVERAGE(A1:A3) |
9.000000 |
Media de rango usando tamaño real del rango. |
=MIN(A1:A4,B1) |
-2.000000 |
Mínimo con rango y celda suelta. |
=MAX(A1:A4,B1) |
12.000000 |
Máximo con rango y celda suelta. |
=COUNT(A1:A3,B1) |
4.000000 |
Conteo de elementos expandiendo rangos. |
=PRODUCT(A1:A3) |
600.000000 |
Producto de todos los valores de un rango. |
=ABS(A4) |
2.000000 |
Función unaria ABS. |
=FLOOR(3.9) |
3.000000 |
Función unaria FLOOR. |
=CEIL(3.1) |
4.000000 |
Función unaria CEIL. |
=ROUND(3.6) |
4.000000 |
Función unaria ROUND. |
=IF(A1>A2,100,200) |
100.000000 |
IF con condición verdadera. |
=IF(A1<A2,100,200) |
200.000000 |
IF con condición falsa. |
=SUM(A1:B1) |
16.000000 |
Rango horizontal válido. |
=Z99+ZZZZ1 |
3000.000000 |
Referencias con columnas largas. |
=C1 |
Error semántico: celda sin valor asignado | Referencia a celda inexistente. |
=SUM(A1:B2) |
Error semántico: no se admiten rangos matriciales | Rechazar rangos rectangulares. |
=SUM(A3:A1) |
Error semántico: inconsistencia en el acceso | Rechazar rangos invertidos. |
=SUM(A1:A5) |
Error semántico: celda sin valor asignado | Rango con alguna celda inexistente. |
=ABS(A1:A3) |
Error semántico: función no acepta rangos | Rechazar uso de rango en una función unaria. |
=ABS(A1,A2) |
Error semántico: esperaba un único argumento | Validación de aridad en funciones unarias. |
=IF(A1,100,200) |
Error semántico: esperaba una comparación | Validación de condición de IF. |
=IF(A1>A2,100) |
Error semántico: IF esperaba 3 argumentos |
Validación de aridad de IF. |
=UNKNOWN(A1) |
Error semántico: función no definida | Rechaza funciones no definidas. |
SUM(A1:A3) |
Error de sintaxis: se esperaba = o ? |
Consulta sin símbolo inicial. |
=SUM(A1,) |
Error de sintaxis: coma mal situada | Coma antes de cerrar argumentos. |
=SUM(A1 A2) |
Error de sintaxis: falta operador | Dos expresiones contiguas sin operador. |
=SUM(A1:A3 |
Error de sintaxis: se esperaba ) |
Paréntesis desbalanceado. |
=SUM(A1:) |
Error de sintaxis: formato de rango incorrecto | Rango incompleto. |
?SUM |
Muestra ayuda de SUM |
Consulta de ayuda válida. |
? |
Error de sintaxis: se esperaba función | Consulta de ayuda incompleta. |
Estas limitaciones coinciden con recomendación del enunciado (y algunas propuestas mías): acotar el lenguaje, simplificar el modelo de rangos y usar una estructura sencilla para consultar valores de celdas.
- Solo se trabaja con valores numéricos. No hay cadenas, fechas, booleanos como tipo separado ni formato de celda. Las comparaciones devuelven un valor lógico suficiente para resolver
IF, pero internamente todo se maneja como valores numéricos. - Los rangos están acotados a una fila o una columna. Se admiten rangos como
A1:A5oA1:C1, pero se rechazan rangos matriciales comoA1:C3. - No hay dependencias entre fórmulas almacenadas. El fichero de datos carga valores fijos de celdas; no se guardan fórmulas dentro de celdas ni se recalculan dependencias.
- El modo interactivo es simple. Lee una línea, procesa una fórmula o termina con
EXIT. No implementa edición avanzada propia, sesiones persistentes ni comandos adicionales de gestión. - La validación de funciones es semántica. Sintácticamente cualquier identificador en mayúsculas puede ser una función; si no está implementada se rechaza después.
- Los números aceptados son reales sencillos. No se admite notación científica ni formatos alternativos.
- No se implementa recuperación de errores. Ante una entrada incorrecta se informa del error y se pasa a la siguiente línea del intérprete.
Además de los requisitos básicos, se han añadido algunas mejoras:
- Construcción de un AST. Considero que el uso del AST es una mejora de arquitectura: permite separar claramente análisis sintáctico, análisis semántico y evaluación. No añade una función nueva al usuario, pero hace que el intérprete sea más mantenible y evita evaluar mediante sustituciones textuales o recorridos ad hoc.
- Mensajes de error personalizados. Se personalizan errores léxicos, sintácticos y semánticos para que el usuario entienda mejor qué parte de la entrada es incorrecta.
- Documentación de funciones con
?. Se añade una consulta de ayuda como?SUM, que muestra documentación específica de la función. - Soporte de historial y lectura interactiva con
readline. Aunque el enunciado solo exige un bucle interactivo simple, el uso dereadlinemejora la experiencia de uso permitiendo editar la línea actual o acceder al historial de expresiones con las flechas.
Como trabajo futuro se podrían añadir:
- Rangos rectangulares completos (
A1:C3). - Notación científica y más formatos numéricos.
- Tipos adicionales, como texto o booleanos explícitos.
- Más funciones compatibles con hojas de cálculo reales.
Durante el desarrollo de la práctica se ha utilizado inteligencia artificial como herramienta de apoyo. Su uso se ha limitado a tareas auxiliares y de orientación, manteniendo la integración final dentro del diseño del proyecto.
En concreto, se ha usado para:
- Obtener recomendaciones sobre la estructura del proyecto, separación en ficheros fuente, cabeceras, organización del
Makefiley limpieza general del código. - Generar borradores de los mensajes de ayuda que se muestran con consultas como
?SUM,?IFo?ABS. - Apoyar la implementación del acceso a rangos, especialmente en funciones auxiliares como la conversión de referencias a coordenadas y la generación de las celdas intermedias de un rango.
- Revisar y refinar partes de la documentación de la memoria.
Para ello, se ha usado el agente Codex de Open AI.