Semana 14 — Análisis léxico y sintáctico
Objetivos
- Partir un texto en tokens.
- Leer una gramática y escribir un parser descendente recursivo a partir de ella.
- Reportar errores de sintaxis con un mensaje útil y sin fugas.
Teoría — Análisis léxico y análisis sintáctico descendente recursivo
Lectura: Apuntes, semana 14.
Temas:
- Las etapas de un intérprete: texto, tokens, árbol, resultado.
- Análisis léxico: leer números, nombres y símbolos.
- Gramáticas: reglas para expresiones, y cómo una gramática fija la precedencia y la asociatividad.
- Parser descendente recursivo: una función por regla. La recursión de la gramática se vuelve recursión del programa.
- Errores de sintaxis: indicar la posición y liberar lo ya construido.
- El lenguaje Mini: números, variables,
let,if,def,printy llamadas a funciones.
Laboratorio — Parser del lenguaje Mini
Esta semana una sesión es para la Práctica 4.
Sesión 1:
- Leer el analizador léxico dado y sus pruebas, y agregarle comparadores y comentarios.
- Escribir el parser de expresiones con precedencia y paréntesis, que produce el
Exprde la semana 13. - Agregar mensajes de error con línea y columna. Las pruebas exigen que no haya fugas cuando falla.
Sesión 2: Práctica 4.
Entregas
- Práctica 4: Backtracking, funciones como valores y datos recursivos. Ver
practicas/README.md.
Preguntas de repaso
- ¿Qué tokens produce
let x1 = 3*(y+2)? - ¿Qué parte de la gramática hace que
1 + 2 * 3valga 7? - ¿Por qué una regla como
expr → expr + termno se puede escribir tal cual en un parser descendente? - Si el parser falla a la mitad de una expresión, ¿qué hay que liberar?
Código de la semana
Compilar y probar dentro del contenedor del curso con make y make test.
Ejemplos (2)Errores (2)Pruebas (3)Laboratorio (12)Compilación (1)
Ejemplos
Los programas que usan los apuntes y la clase.
// Parser de expresiones lógicas: el ejemplo de la clase.
//
// or_rule → and_rule ("or" and_rule)*
// and_rule → not_rule ("and" not_rule)*
// not_rule → "not" not_rule | atom
// atom → "true" | "false" | "(" or_rule ")"
//
// Usa el analizador léxico de lab/: and, or, not, true y false le llegan como TOKEN_NAME.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "lexer.h"
typedef enum { LOGIC_VALUE, LOGIC_NOT, LOGIC_AND, LOGIC_OR } LogicKind;
typedef struct logic Logic;
struct logic {
LogicKind kind;
int value; // solo en LOGIC_VALUE
Logic *left; // LOGIC_NOT usa solo left
Logic *right;
};
typedef struct {
int line;
int column;
char message[128];
} LogicError;
typedef struct {
Lexer *lexer;
Token current;
LogicError *error;
} Parser;
static void logic_free(Logic *node) {
if (node == NULL) {
return;
}
logic_free(node->left);
logic_free(node->right);
free(node);
}
// Toma posesión de los hijos, incluso si falla.
static Logic *logic_new(LogicKind kind, int value, Logic *left, Logic *right) {
Logic *node = malloc(sizeof *node);
if (node == NULL) {
logic_free(left);
logic_free(right);
return NULL;
}
node->kind = kind;
node->value = value;
node->left = left;
node->right = right;
return node;
}
static void advance(Parser *p) {
p->current = lexer_next(p->lexer);
}
static int is_word(const Parser *p, const char *word) {
return p->current.kind == TOKEN_NAME && strcmp(p->current.text, word) == 0;
}
static void fail(Parser *p, const char *message) {
p->error->line = p->current.line;
p->error->column = p->current.column;
const char *text = p->current.kind == TOKEN_ERROR ? p->current.text : message;
snprintf(p->error->message, sizeof p->error->message, "%s", text);
}
static Logic *parse_or(Parser *p);
static Logic *parse_atom(Parser *p) {
if (is_word(p, "true") || is_word(p, "false")) {
Logic *leaf = logic_new(LOGIC_VALUE, is_word(p, "true"), NULL, NULL);
if (leaf == NULL) {
fail(p, "no hay memoria");
return NULL;
}
advance(p);
return leaf;
}
if (p->current.kind == TOKEN_LPAREN) {
advance(p);
Logic *inside = parse_or(p);
if (inside == NULL) {
return NULL;
}
if (p->current.kind != TOKEN_RPAREN) {
fail(p, "se esperaba ')'");
logic_free(inside);
return NULL;
}
advance(p);
return inside;
}
fail(p, "se esperaba true, false o '('");
return NULL;
}
static Logic *parse_not(Parser *p) {
if (!is_word(p, "not")) {
return parse_atom(p);
}
advance(p);
Logic *operand = parse_not(p);
if (operand == NULL) {
return NULL;
}
Logic *node = logic_new(LOGIC_NOT, 0, operand, NULL);
if (node == NULL) {
fail(p, "no hay memoria");
}
return node;
}
static Logic *parse_and(Parser *p) {
Logic *left = parse_not(p);
if (left == NULL) {
return NULL;
}
while (is_word(p, "and")) {
advance(p);
Logic *right = parse_not(p);
if (right == NULL) {
logic_free(left);
return NULL;
}
left = logic_new(LOGIC_AND, 0, left, right);
if (left == NULL) {
fail(p, "no hay memoria");
return NULL;
}
}
return left;
}
static Logic *parse_or(Parser *p) {
Logic *left = parse_and(p);
if (left == NULL) {
return NULL;
}
while (is_word(p, "or")) {
advance(p);
Logic *right = parse_and(p);
if (right == NULL) {
logic_free(left);
return NULL;
}
left = logic_new(LOGIC_OR, 0, left, right);
if (left == NULL) {
fail(p, "no hay memoria");
return NULL;
}
}
return left;
}
static Logic *parse_logic(const char *source, LogicError *error) {
Parser parser = {.lexer = lexer_new(source), .error = error};
if (parser.lexer == NULL) {
snprintf(error->message, sizeof error->message, "no hay memoria");
return NULL;
}
advance(&parser);
Logic *tree = parse_or(&parser);
if (tree != NULL && parser.current.kind != TOKEN_END) {
fail(&parser, "sobra texto después de la expresión");
logic_free(tree);
tree = NULL;
}
lexer_free(parser.lexer);
return tree;
}
// Escribe el árbol con todos sus paréntesis, para que se vea su forma.
static void logic_print(const Logic *node) {
switch (node->kind) {
case LOGIC_VALUE:
printf("%s", node->value ? "true" : "false");
break;
case LOGIC_NOT:
printf("(not ");
logic_print(node->left);
printf(")");
break;
case LOGIC_AND:
case LOGIC_OR:
printf("(");
logic_print(node->left);
printf(" %s ", node->kind == LOGIC_AND ? "and" : "or");
logic_print(node->right);
printf(")");
break;
}
}
static int logic_eval(const Logic *node) {
switch (node->kind) {
case LOGIC_VALUE: return node->value;
case LOGIC_NOT: return !logic_eval(node->left);
case LOGIC_AND: return logic_eval(node->left) && logic_eval(node->right);
case LOGIC_OR: return logic_eval(node->left) || logic_eval(node->right);
}
return 0;
}
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "uso: %s 'expresión lógica'\n", argv[0]);
return 1;
}
LogicError error;
Logic *tree = parse_logic(argv[1], &error);
if (tree == NULL) {
printf("línea %d, columna %d: %s\n", error.line, error.column, error.message);
return 1;
}
printf("árbol: ");
logic_print(tree);
printf("\nvalor: %s\n", logic_eval(tree) ? "true" : "false");
logic_free(tree);
return 0;
}// Dos formas de leer una cadena de restas, sin construir un árbol: se calcula al vuelo.
//
// por la derecha: chain → NUMBER [ "-" chain ]
// con un ciclo: chain → NUMBER ("-" NUMBER)*
#include <stdio.h>
#include "lexer.h"
typedef struct {
Lexer *lexer;
Token current;
int failed;
} Parser;
static void advance(Parser *p) {
p->current = lexer_next(p->lexer);
}
static double read_number(Parser *p) {
if (p->current.kind != TOKEN_NUMBER) {
p->failed = 1;
return 0.0;
}
double value = p->current.number;
advance(p);
return value;
}
static double chain_right(Parser *p) {
double value = read_number(p);
if (p->current.kind == TOKEN_MINUS) {
advance(p);
return value - chain_right(p);
}
return value;
}
static double chain_loop(Parser *p) {
double value = read_number(p);
while (p->current.kind == TOKEN_MINUS) {
advance(p);
value = value - read_number(p);
}
return value;
}
// Corre una de las dos funciones sobre el texto. Devuelve 1 si lo pudo leer completo.
static int run_chain(const char *source, double (*chain)(Parser *), double *value) {
Parser parser = {.lexer = lexer_new(source), .failed = 0};
if (parser.lexer == NULL) {
return 0;
}
advance(&parser);
*value = chain(&parser);
int ok = !parser.failed && parser.current.kind == TOKEN_END;
lexer_free(parser.lexer);
return ok;
}
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "uso: %s 'a - b - c'\n", argv[0]);
return 1;
}
double right;
double loop;
if (!run_chain(argv[1], chain_right, &right) || !run_chain(argv[1], chain_loop, &loop)) {
printf("solo entiendo números separados por -\n");
return 1;
}
printf("recursión por la derecha: %g\n", right);
printf("con un ciclo: %g\n", loop);
return 0;
}Errores
Ejemplos con errores a propósito: no compilan, fallan o dan un resultado incorrecto. Los apuntes explican cada uno.
// Una regla or_rule → atom ("or" atom)* que olvida liberar cuando falla la derecha.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "lexer.h"
typedef struct node {
struct node *left;
struct node *right;
} Node;
typedef struct {
Lexer *lexer;
Token current;
} Parser;
static void advance(Parser *p) {
p->current = lexer_next(p->lexer);
}
static void node_free(Node *node) {
if (node != NULL) {
node_free(node->left);
node_free(node->right);
free(node);
}
}
static Node *node_new(Node *left, Node *right) {
Node *node = malloc(sizeof *node);
if (node == NULL) {
node_free(left);
node_free(right);
return NULL;
}
node->left = left;
node->right = right;
return node;
}
static Node *parse_atom(Parser *p) {
if (p->current.kind != TOKEN_NAME) {
return NULL;
}
advance(p);
return node_new(NULL, NULL);
}
static Node *parse_or(Parser *p) {
Node *left = parse_atom(p);
while (left != NULL && p->current.kind == TOKEN_NAME && strcmp(p->current.text, "or") == 0) {
advance(p);
Node *right = parse_atom(p);
if (right == NULL) {
return NULL; // ¿y left?
}
left = node_new(left, right);
}
return left;
}
int main(void) {
Parser parser = {.lexer = lexer_new("true or")};
if (parser.lexer == NULL) {
return 1;
}
advance(&parser);
Node *tree = parse_or(&parser);
printf("%s\n", tree == NULL ? "error de sintaxis" : "bien");
node_free(tree);
lexer_free(parser.lexer);
return 0;
}// La regla chain → chain "-" NUMBER | NUMBER, copiada tal cual. No termina.
#include <stdio.h>
#include "lexer.h"
typedef struct {
Lexer *lexer;
Token current;
} Parser;
static void advance(Parser *p) {
p->current = lexer_next(p->lexer);
}
static double parse_chain(Parser *p) {
double left = parse_chain(p); // primero la regla misma, sin consumir nada
if (p->current.kind == TOKEN_MINUS) {
advance(p);
double right = p->current.number;
advance(p);
return left - right;
}
return left;
}
int main(void) {
Parser parser = {.lexer = lexer_new("1 - 2 - 3")};
if (parser.lexer == NULL) {
return 1;
}
advance(&parser);
printf("%g\n", parse_chain(&parser));
lexer_free(parser.lexer);
return 0;
}Pruebas
Comprueban lo que afirman los apuntes; se ejecutan con make test.
"""Comprueba los programas de la clase y lo que los apuntes dicen de las herramientas."""
import pathlib
import subprocess
import tempfile
import unittest
HERE = pathlib.Path(__file__).parent
BUILD = HERE / "build"
FLAGS = ["-Wall", "-Wextra", "-std=c17", "-g", "-fsanitize=address,undefined", "-Ilab"]
PROGRAMS = ["logic_parser", "subtract_chain"]
def run(program, *args):
done = subprocess.run(
[str(program), *args], cwd=HERE, text=True, encoding="utf-8", errors="replace",
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=30,
)
return done.stdout, done.returncode
def build(source):
"""Compila y devuelve (ruta del ejecutable, carpeta temporal, mensajes de gcc)."""
folder = tempfile.TemporaryDirectory()
binary = pathlib.Path(folder.name) / "program"
done = subprocess.run(
["gcc", *FLAGS, source, "lab/lexer.c", "-o", str(binary)], cwd=HERE, text=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
)
if done.returncode != 0:
raise AssertionError(done.stdout)
return binary, folder, done.stdout
class Programs(unittest.TestCase):
def test_every_program_compiles_without_warnings(self):
for name in PROGRAMS:
with self.subTest(program=name):
subprocess.run(
["gcc", *FLAGS, "-Werror", "-c", f"{name}.c", "-o", "/dev/null"],
cwd=HERE, check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
)
class Logic(unittest.TestCase):
def test_and_binds_tighter_than_or(self):
out, code = run(BUILD / "logic_parser", "true or false and false")
self.assertEqual(code, 0, out)
self.assertIn("(true or (false and false))", out)
self.assertIn("valor: true", out)
def test_loop_associates_to_the_left(self):
out, code = run(BUILD / "logic_parser", "true or false or true")
self.assertEqual(code, 0, out)
self.assertIn("((true or false) or true)", out)
def test_every_error_is_reported_without_leaks(self):
cases = {
"true or": "columna 8",
"true and": "columna 9",
"not": "columna 4",
"(true": "columna 6",
"(true or (false and": "columna 20",
"true or false and (true or": "columna 27",
"true maybe": "columna 6",
"true @": "columna 6",
"": "columna 1",
}
for source, column in cases.items():
with self.subTest(source=source):
out, code = run(BUILD / "logic_parser", source)
self.assertEqual(code, 1, out)
self.assertIn(column, out)
self.assertNotIn("Sanitizer", out)
def test_error_on_a_later_line(self):
out, code = run(BUILD / "logic_parser", "true or\n @")
self.assertEqual(code, 1, out)
self.assertIn("línea 2, columna 3", out)
class Associativity(unittest.TestCase):
def test_right_recursion_gets_the_wrong_value(self):
out, code = run(BUILD / "subtract_chain", "1 - 2 - 3")
self.assertEqual(code, 0, out)
self.assertEqual(out.split(), ["recursión", "por", "la", "derecha:", "2", "con", "un", "ciclo:", "-4"])
def test_they_agree_with_a_single_subtraction(self):
out, code = run(BUILD / "subtract_chain", "7 - 2")
self.assertEqual(code, 0, out)
self.assertEqual(out.split().count("5"), 2)
def test_bad_input(self):
out, code = run(BUILD / "subtract_chain", "1 - x")
self.assertEqual(code, 1, out)
self.assertNotIn("Sanitizer", out)
class Tools(unittest.TestCase):
def test_left_recursion_warns_and_overflows(self):
binary, folder, messages = build("errors/left_recursion.c")
with folder:
self.assertIn("infinite recursion", messages)
out, code = run(binary)
self.assertNotEqual(code, 0)
self.assertIn("stack-overflow", out)
self.assertIn("parse_chain", out)
def test_forgotten_free_is_a_leak(self):
binary, folder, messages = build("errors/leak_on_error.c")
with folder:
self.assertEqual(messages, "")
out, code = run(binary)
self.assertNotEqual(code, 0)
self.assertIn("detected memory leaks", out)
def test_too_many_parentheses_overflow_the_stack(self):
out, code = run(BUILD / "logic_parser", "(" * 100000)
self.assertNotEqual(code, 0)
self.assertIn("stack-overflow", out)
def test_a_thousand_parentheses_are_fine(self):
out, code = run(BUILD / "logic_parser", "(" * 1000 + "true" + ")" * 1000)
self.assertEqual(code, 0, out)
self.assertIn("valor: true", out)
if __name__ == "__main__":
unittest.main()"""Pruebas del laboratorio. LAB_DIR dice dónde está el código: lab o solution.
Cada grupo compila un programa propio con los archivos de esa carpeta. Corren con ASan y UBSan:
una fuga, o un aviso como un overflow, también hacen fallar la prueba.
"""
import functools
import os
import pathlib
import re
import subprocess
import tempfile
import unittest
HERE = pathlib.Path(__file__).parent
LAB_DIR = os.environ.get("LAB_DIR", "solution")
SOURCE = HERE / LAB_DIR
LAB = HERE / "lab"
FLAGS = ["-Wall", "-Wextra", "-std=c17", "-g", "-fsanitize=address,undefined"]
KEEP = []
def source(name):
"""El archivo de la carpeta que se prueba. Si no está ahí, es uno de los ya hechos de lab/."""
path = SOURCE / name
return path if path.exists() else LAB / name
def build(main_source, sources, extra=()):
"""Compila en una carpeta temporal y devuelve la ruta del ejecutable."""
folder = tempfile.TemporaryDirectory()
KEEP.append(folder)
path = pathlib.Path(folder.name)
inputs = list(map(str, sources))
if main_source is not None:
driver = path / "driver.c"
driver.write_text(main_source, encoding="utf-8")
inputs.insert(0, str(driver))
binary = path / "driver"
built = subprocess.run(
["gcc", *FLAGS, f"-I{LAB}", *inputs, *extra, "-o", str(binary)],
text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
)
if built.returncode != 0:
raise AssertionError(f"No compila:\n{built.stdout}")
return binary
def run(binary, *args):
"""Devuelve (salida, código de salida)."""
try:
done = subprocess.run(
[str(binary), *args], text=True, encoding="utf-8", errors="replace",
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=20,
)
except subprocess.TimeoutExpired:
raise AssertionError(f"El programa no terminó en 20 segundos, con la entrada {args!r}.") from None
return done.stdout, done.returncode
def writable_globals(path):
"""Los símbolos de datos que se pueden escribir: las variables globales del archivo."""
with tempfile.TemporaryDirectory() as name:
obj = pathlib.Path(name) / "file.o"
subprocess.run(["gcc", "-std=c17", "-fno-pic", f"-I{LAB}", "-c", str(path), "-o", str(obj)], check=True)
done = subprocess.run(["nm", str(obj)], text=True, stdout=subprocess.PIPE, check=True)
return [line.split()[-1] for line in done.stdout.splitlines() if re.search(r" [bBdD] ", line)]
@functools.lru_cache(maxsize=None)
def tokens_program():
return build(None, [LAB / "show_tokens.c", source("lexer.c")])
# Analiza argv[1]. Escribe "árbol | valor", o "ERROR línea columna | mensaje".
PARSE_MAIN = r"""
#include <stdio.h>
#include "env.h"
#include "expr.h"
#include "parser.h"
int main(int argc, char *argv[]) {
if (argc != 2) { return 2; }
ParseError error;
Expr *tree = parse_expression(argv[1], &error);
if (tree == NULL) {
printf("ERROR %d %d | %s\n", error.line, error.column, error.message);
return 0;
}
expr_print(tree, stdout);
Env *env = env_new(NULL);
double value = 0.0;
if (env == NULL || !env_set(env, "x", 2.0) || !env_set(env, "y", 3.0) || !env_set(env, "x1_y", 10.0)
|| !env_set(env, "_z", 20.0)) { return 2; }
if (expr_eval(tree, env, &value)) { printf(" | %g\n", value); } else { printf(" | no evalúa\n"); }
env_free(env);
expr_free(tree);
return 0;
}
"""
@functools.lru_cache(maxsize=None)
def parse_program():
return build(PARSE_MAIN, [source("parser.c"), source("lexer.c"), LAB / "expr.c", LAB / "env.c"])
def parsed(text):
out, code = run(parse_program(), text)
if code != 0:
raise AssertionError(f"Con la entrada {text!r} el programa terminó con error:\n{out}")
return out.strip()
class LabCase(unittest.TestCase):
longMessage = False
class Lexer(LabCase):
"""Parte 1: comparadores y comentarios."""
def tokens(self, text):
out, code = run(tokens_program(), text)
if code != 0:
self.fail(f"Con la entrada {text!r} el programa terminó con error:\n{out}")
found = []
for line in out.splitlines():
parts = line.split(None, 2)
position, kind = parts[0], parts[1]
found.append((kind, parts[2] if len(parts) > 2 else "", position))
return found
def kinds(self, text):
return [kind for kind, _, _ in self.tokens(text)]
def check_kinds(self, text, wanted):
got = self.kinds(text)
wanted = wanted.split()
if got != wanted:
self.fail(f"Tokens de {text!r}\n se esperaba: {wanted}\n se obtuvo: {got}")
def test_each_comparator(self):
cases = {"<": "LESS", "<=": "LESS_EQUAL", ">": "GREATER", ">=": "GREATER_EQUAL",
"==": "EQUAL", "!=": "NOT_EQUAL", "=": "ASSIGN"}
for text, kind in cases.items():
with self.subTest(text=text):
self.check_kinds(text, f"{kind} END")
def test_comparator_text(self):
for text in ("<", "<=", ">", ">=", "==", "!="):
with self.subTest(text=text):
self.assertEqual(self.tokens(text)[0][1], text, f"El text del token {text} debe ser {text}.")
def test_comparators_without_spaces(self):
self.check_kinds("a<=b", "NAME LESS_EQUAL NAME END")
self.check_kinds("a!=1", "NAME NOT_EQUAL NUMBER END")
self.check_kinds("1>2", "NUMBER GREATER NUMBER END")
def test_longest_token_first(self):
self.check_kinds("===", "EQUAL ASSIGN END")
self.check_kinds("<<=", "LESS LESS_EQUAL END")
self.check_kinds(">==", "GREATER_EQUAL ASSIGN END")
def test_two_characters_must_be_together(self):
self.check_kinds("< =", "LESS ASSIGN END")
self.check_kinds("= =", "ASSIGN ASSIGN END")
def test_lone_exclamation_is_an_error(self):
self.check_kinds("! =", "ERROR ASSIGN END")
self.check_kinds("1 ! 2", "NUMBER ERROR NUMBER END")
kind, text, position = self.tokens(" !")[0]
self.assertEqual((kind, position), ("ERROR", "1:3"))
self.assertIn("!", text, "El mensaje de error debe decir qué carácter encontró.")
def test_column_after_a_comparator(self):
self.assertEqual(self.tokens("x <= y")[2][2], "1:6", "Un comparador de dos caracteres debe avanzar dos columnas.")
self.assertEqual(self.tokens("x != y")[2][2], "1:6", "Un comparador de dos caracteres debe avanzar dos columnas.")
def test_comments(self):
self.check_kinds("# solo esto", "END")
self.check_kinds("1 # uno", "NUMBER END")
self.check_kinds("1 # < 2 @ !\n2", "NUMBER NUMBER END")
self.check_kinds("#\n#\n3", "NUMBER END")
self.check_kinds("x#y\nz", "NAME NAME END")
self.check_kinds("# año\n1", "NUMBER END")
def test_a_comment_keeps_the_line_count(self):
self.assertEqual(self.tokens("1 # uno\n2")[1][2], "2:1", "Después de un comentario, la línea debe seguir contando.")
self.assertEqual(self.tokens("# a\n# b\n x")[0][2], "3:3", "Después de un comentario, la línea debe seguir contando.")
def test_end_after_a_comment_without_newline(self):
self.assertEqual(self.tokens("# abc")[0][2], "1:6", "TOKEN_END va donde termina el texto.")
def test_no_globals(self):
names = writable_globals(source("lexer.c"))
if names:
self.fail(f"lexer.c no debe tener variables globales. Encontré: {names}")
def test_their_tests_pass(self):
path = source("lexer_tests.c")
text = re.sub(r"//.*", "", path.read_text(encoding="utf-8"))
for piece, what in (("TOKEN_LESS_EQUAL", "los comparadores"), ("#", "los comentarios")):
body = re.sub(r"^\s*#include.*$", "", text, flags=re.M)
if piece not in body:
self.fail(f"lexer_tests.c todavía no prueba {what}.")
out, code = run(build(None, [path, source("lexer.c")]))
self.assertEqual(code, 0, out)
class Trees(LabCase):
"""Parte 2: lo que se escribe es el árbol, con solo los paréntesis necesarios, y su valor."""
def check(self, text, wanted, why=""):
got = parsed(text)
if got != wanted:
self.fail(f"Con la entrada {text!r}\n se esperaba: {wanted}\n se obtuvo: {got}\n{why}".rstrip())
def test_primary(self):
self.check("42", "42 | 42")
self.check("3.25", "3.25 | 3.25")
self.check("x", "x | 2")
self.check("x1_y + _z", "x1_y + _z | 30")
self.check("desconocida", "desconocida | no evalúa")
def test_parentheses_disappear_from_the_tree(self):
self.check("(7)", "7 | 7")
self.check("((x))", "x | 2")
self.check("1 + (2 * 3)", "1 + 2 * 3 | 7")
def test_precedence(self):
self.check("1 + 2 * 3", "1 + 2 * 3 | 7", "* debe ir más abajo en el árbol que +.")
self.check("2 * 3 + 1", "2 * 3 + 1 | 7")
self.check("(1 + 2) * 3", "(1 + 2) * 3 | 9")
self.check("1 + 6 / 2 - 1", "1 + 6 / 2 - 1 | 3")
def test_left_associativity(self):
why = "Los operadores asocian por la izquierda: se necesita un ciclo, no recursión por la derecha."
self.check("1 - 2 - 3", "1 - 2 - 3 | -4", why)
self.check("8 / 4 / 2", "8 / 4 / 2 | 1", why)
self.check("1 + 2 - 3 + 4", "1 + 2 - 3 + 4 | 4", why)
self.check("1 - (2 - 3)", "1 - (2 - 3) | 2")
def test_unary_minus(self):
why = "-x se construye como 0 - x."
self.check("-x", "0 - x | -2", why)
self.check("--x", "0 - (0 - x) | 2", why)
self.check("-2 * 3", "(0 - 2) * 3 | -6", why)
self.check("2 * -3", "2 * (0 - 3) | -6", why)
self.check("-(1 + 2)", "0 - (1 + 2) | -3", why)
self.check("1 - -1", "1 - (0 - 1) | 2", why)
def test_comparisons(self):
self.check("1 < 2", "1 < 2 | 1")
self.check("2 <= 1", "2 <= 1 | 0")
self.check("2 > 1", "2 > 1 | 1")
self.check("1 >= 2", "1 >= 2 | 0")
self.check("x == 2", "x == 2 | 1")
self.check("x != 2", "x != 2 | 0")
def test_comparisons_are_below_sums(self):
self.check("1 + 2 < 2 * 2", "1 + 2 < 2 * 2 | 1")
self.check("(1 < 2) + 1", "(1 < 2) + 1 | 2")
self.check("(1 < 2) == 1", "(1 < 2) == 1 | 1")
self.check("1 == (2 > 1)", "1 == (2 > 1) | 1")
def test_spread_over_lines_with_comments(self):
self.check("1 + # uno\n 2 * # dos\n 3", "1 + 2 * 3 | 7")
def test_long_expressions(self):
self.check("+".join(["1"] * 500), " + ".join(["1"] * 500) + " | 500")
self.check("(" * 200 + "x" + ")" * 200, "x | 2")
def test_exact_tree(self):
body = r"""
#include <stdio.h>
#include "parser.h"
int main(void) {
ParseError error;
Expr *a = parse_expression("1 + x * -y", &error);
Expr *b = expr_binary(OP_ADD, expr_number(1), expr_binary(OP_MUL, expr_variable("x"),
expr_binary(OP_SUB, expr_number(0), expr_variable("y"))));
if (a == NULL || b == NULL) { return 1; }
printf("%d\n", expr_equal(a, b));
expr_free(a);
expr_free(b);
return 0;
}
"""
program = build(body, [source("parser.c"), source("lexer.c"), LAB / "expr.c", LAB / "env.c"])
out, code = run(program)
self.assertEqual(code, 0, out)
self.assertEqual(out.strip(), "1", "El árbol de 1 + x * -y no tiene la forma esperada.")
def test_no_globals(self):
names = writable_globals(source("parser.c"))
if names:
self.fail(f"parser.c no debe tener variables globales. Encontré: {names}")
class Errors(LabCase):
"""Parte 3: posición, mensaje y nada sin liberar. Las fugas las detecta ASan."""
def check(self, text, line, column, contains=None):
got = parsed(text)
match = re.fullmatch(r"ERROR (-?\d+) (-?\d+) \| (.*)", got, re.S)
if match is None:
self.fail(f"La entrada {text!r} debe dar un error, y se obtuvo: {got}")
where = (int(match.group(1)), int(match.group(2)))
if where != (line, column):
self.fail(f"Con la entrada {text!r}, el error debe estar en la línea {line}, columna {column}.\n"
f" se obtuvo: línea {where[0]}, columna {where[1]}: {match.group(3)}")
if not match.group(3).strip():
self.fail(f"Con la entrada {text!r}, el mensaje de error está vacío.")
if contains is not None and contains not in match.group(3):
self.fail(f"Con la entrada {text!r}, el mensaje debe mencionar {contains}. Se obtuvo: {match.group(3)}")
def test_missing_operand(self):
self.check("1 +", 1, 4)
self.check("1 + * 2", 1, 5)
self.check("* 2", 1, 1)
self.check("1 <", 1, 4)
self.check("-", 1, 2)
def test_empty(self):
self.check("", 1, 1)
self.check(" ", 1, 4)
self.check("# nada\n", 2, 1)
def test_parentheses(self):
self.check("(1 + 2", 1, 7, ")")
self.check("(", 1, 2)
self.check("()", 1, 2)
self.check("((1)", 1, 5, ")")
def test_leftover_text(self):
self.check("1 2", 1, 3)
self.check("1)", 1, 2)
self.check("x y", 1, 3)
self.check("x = 1", 1, 3)
def test_comparisons_do_not_chain(self):
self.check("1 < 2 < 3", 1, 7)
self.check("1 == 2 != 3", 1, 8)
def test_not_this_week(self):
self.check("if x", 1, 1)
self.check("let", 1, 1)
self.check("f(1)", 1, 2)
def test_position_on_later_lines(self):
self.check("x\n+\n)", 3, 1)
self.check("1 +\n", 2, 1)
self.check("1 + # uno\n# dos\n *", 3, 3)
self.check("\t1 +\t)", 1, 6)
def test_lexer_errors_keep_their_message(self):
self.check("1 + @", 1, 5, "@")
self.check("@", 1, 1, "@")
self.check("(1 $", 1, 4, "$")
self.check("1 ! 2", 1, 3, "!")
self.check("1.5.2", 1, 4, ".")
def test_nothing_leaks_when_it_fails_late(self):
cases = ("1 + 2 * (3 +", "(1 + 2", "x + y * z +", "x * y *", "-(1 + 2", "- - x +", "1 + 2 <", "1 + 2 < 3 +",
"x + @", "(x + y) * (y + x", "((1 + 2) * 3", "x < (y", "1 2", "x + y z")
for text in cases:
with self.subTest(text=text):
self.assertTrue(parsed(text).startswith("ERROR"), f"La entrada {text!r} debe dar un error.")
def test_first_error_is_the_one_reported(self):
self.check("(1 + ) )", 1, 6)
self.check("* @", 1, 1)
def test_without_memory(self):
body = r"""
#include <stdio.h>
#include <stdlib.h>
#include "parser.h"
void *__real_malloc(size_t size);
static long remaining = -1;
static long calls = 0;
void *__wrap_malloc(size_t size) {
calls++;
if (remaining == 0) { remaining = -1; return NULL; }
if (remaining > 0) { remaining--; }
return __real_malloc(size);
}
int main(void) {
const char *text = "1 + x * (2 - y) < -3";
ParseError error;
Expr *tree = parse_expression(text, &error);
if (tree == NULL) { puts("no analiza"); return 0; }
expr_free(tree);
long total = calls;
for (long i = 0; i < total; i++) {
remaining = i;
tree = parse_expression(text, &error);
remaining = -1;
if (tree != NULL) { printf("devolvió un árbol sin memoria, en el malloc %ld\n", i); expr_free(tree); return 0; }
if (error.message[0] == '\0') { printf("sin mensaje, en el malloc %ld\n", i); return 0; }
}
printf("bien %s\n", total >= 11 ? "" : "pocas llamadas");
return 0;
}
"""
program = build(body, [source("parser.c"), source("lexer.c"), LAB / "expr.c", LAB / "env.c"],
["-Wl,--wrap=malloc"])
out, code = run(program)
if code != 0 or out.strip() != "bien":
self.fail("Cuando malloc devuelve NULL, parse_expression debe devolver NULL, con un mensaje y sin fugas.\n"
"Revisen lo que devuelven expr_number, expr_variable y expr_binary.\n" + out[:800])
# Lo que se les da hecho.
EXPR_MAIN = r"""
#include <stdio.h>
#include "expr.h"
static void show(Expr *e) { expr_print(e, stdout); putchar('\n'); }
static Expr *num(double v) { return expr_number(v); }
static Expr *var(const char *n) { return expr_variable(n); }
static Expr *bin(BinaryOp op, Expr *a, Expr *b) { return expr_binary(op, a, b); }
int main(void) {
Env *env = env_new(NULL);
if (env == NULL || !env_set(env, "x", 3.0)) { return 1; }
double value = -1.0;
Expr *e = bin(OP_MUL, bin(OP_ADD, var("x"), num(1)), num(2));
show(e);
int ok = expr_eval(e, env, &value);
printf("%d %g\n", ok, value);
Expr *copy = expr_copy(e);
printf("%d\n", expr_equal(e, copy));
expr_free(copy);
expr_free(e);
value = -1.0;
e = bin(OP_DIV, num(1), num(0));
ok = expr_eval(e, env, &value);
printf("%d %g\n", ok, value);
expr_free(e);
e = var("z");
ok = expr_eval(e, env, &value);
printf("%d %g\n", ok, value);
expr_free(e);
e = bin(OP_ADD, num(1), bin(OP_ADD, num(2), num(3)));
show(e);
expr_free(e);
e = bin(OP_SUB, var("x"), bin(OP_SUB, var("x"), num(1)));
show(e);
expr_free(e);
e = bin(OP_LESS, bin(OP_LESS, num(1), num(2)), bin(OP_EQUAL, num(1), num(1)));
show(e);
ok = expr_eval(e, env, &value);
printf("%d %g\n", ok, value);
expr_free(e);
e = expr_simplify(bin(OP_ADD, bin(OP_MUL, var("x"), num(1)), bin(OP_MUL, var("x"), num(0))));
show(e);
expr_free(e);
e = expr_simplify(bin(OP_LESS, bin(OP_ADD, num(1), num(2)), num(4)));
show(e);
expr_free(e);
e = expr_simplify(bin(OP_DIV, num(1), num(0)));
show(e);
expr_free(e);
Expr *f = bin(OP_DIV, bin(OP_MUL, var("x"), var("x")), bin(OP_ADD, var("x"), num(1)));
Expr *d = expr_derive(f, "x");
ok = expr_eval(d, env, &value);
printf("%d %g\n", ok, value);
expr_free(d);
expr_free(f);
f = bin(OP_LESS, var("x"), num(1));
printf("%d\n", expr_derive(f, "x") == NULL);
expr_free(f);
printf("%d\n", bin(OP_ADD, num(1), NULL) == NULL);
expr_free(NULL);
env_free(env);
return 0;
}
"""
class GivenFiles(LabCase):
"""Lo que ya viene hecho en lab/. Estas pruebas pasan desde el inicio."""
def test_expr_module(self):
out, code = run(build(EXPR_MAIN, [LAB / "expr.c", LAB / "env.c"]))
self.assertEqual(code, 0, out)
wanted = ["(x + 1) * 2", "1 8", "1", "0 -1", "0 -1", "1 + (2 + 3)", "x - (x - 1)", "(1 < 2) < (1 == 1)", "1 0", "x", "1", "1 / 0",
"1 0.9375", "1", "1"]
self.assertEqual(out.split("\n")[:-1], wanted)
def lexer_output(self, text):
out, code = run(build(None, [LAB / "show_tokens.c", LAB / "lexer.c"]), text)
self.assertEqual(code, 0, out)
return [line.split() for line in out.splitlines()]
def test_review_question(self):
kinds = [line[1] for line in self.lexer_output("let x1 = 3*(y+2)")]
self.assertEqual(kinds, "LET NAME ASSIGN NUMBER STAR LPAREN NAME PLUS NUMBER RPAREN END".split())
def test_number_forms(self):
kinds = [line[1:] for line in self.lexer_output("1e5 0x10 3. .5")]
self.assertEqual(kinds[:4], [["NUMBER", "1"], ["NAME", "e5"], ["NUMBER", "0"], ["NAME", "x10"]])
self.assertEqual(kinds[4], ["NUMBER", "3"])
self.assertEqual(kinds[5][0], "ERROR")
self.assertEqual(kinds[6][0], "ERROR")
self.assertEqual(kinds[7], ["NUMBER", "5"])
def test_token_length(self):
fits = "a" * 63
self.assertEqual(self.lexer_output(fits)[0][1:], ["NAME", fits])
self.assertEqual(self.lexer_output("a" * 64)[0][1], "ERROR")
self.assertEqual(self.lexer_output("1" * 64)[0][1], "ERROR")
def test_not_ascii(self):
lines = self.lexer_output("ñ")
self.assertEqual(lines[0][1], "ERROR")
def test_end_repeats(self):
body = r"""
#include <stdio.h>
#include "lexer.h"
int main(void) {
Lexer *lexer = lexer_new("");
if (lexer == NULL) { return 1; }
int first = lexer_next(lexer).kind == TOKEN_END;
int second = lexer_next(lexer).kind == TOKEN_END;
int third = lexer_next(lexer).kind == TOKEN_END;
printf("%d %d %d\n", first, second, third);
lexer_free(lexer);
return 0;
}
"""
out, code = run(build(body, [LAB / "lexer.c"]))
self.assertEqual((out.strip(), code), ("1 1 1", 0), out)
def test_given_lexer_tests_compile(self):
build(None, [LAB / "lexer_tests.c", LAB / "lexer.c"], ["-Werror"])
if __name__ == "__main__":
unittest.main()"""Vuelve a correr los comandos que aparecen en los apuntes y en las diapositivas
y comprueba que el código citado existe de verdad en esta carpeta."""
import pathlib
import re
import subprocess
import unittest
HERE = pathlib.Path(__file__).parent
ROOT = HERE.parent.parent
NOTES = ROOT / "notes" / "week-14.md"
SLIDES = ROOT / "slides" / "week-14.tex"
# Antes de un bloque cuya salida cambia con el sistema. No se compara.
SYSTEM_DEPENDENT = "<!-- depende del sistema -->"
# Antes de un extracto de un informe de ASan, UBSan o valgrind: sus líneas deben aparecer
# en la salida real y en ese orden, sin contar direcciones, números de proceso ni rutas.
REPORT = "<!-- extracto de un informe -->"
# Antes de lo que debe escribir el laboratorio: se corre con la solución, en build/solution.
LAB_OUTPUT = "<!-- salida del laboratorio -->"
def normalize(text):
# Según el idioma del entorno, gcc usa comillas tipográficas o rectas.
return text.replace("‘", "'").replace("’", "'")
def run_shell(command):
done = subprocess.run(
["bash", "-c", command], cwd=HERE, text=True, encoding="utf-8", errors="replace",
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=60,
)
return normalize(done.stdout)
def clean_lines(text):
return [line.rstrip() for line in text.splitlines()]
def split_commands(lines):
"""Parte un bloque de terminal en pares (comando, líneas de salida)."""
pairs = []
for line in lines:
if line.startswith("$ "):
pairs.append((line[2:], []))
elif pairs:
pairs[-1][1].append(normalize(line.rstrip()))
return pairs
def note_blocks(language):
"""Devuelve (líneas, marca) de cada bloque de los apuntes."""
blocks, current, mark, previous = [], None, "", ""
for raw in NOTES.read_text(encoding="utf-8").splitlines():
line = re.sub(r"^> ?", "", raw)
if current is None:
if line.strip() == "```" + language:
current, mark = [], previous.strip()
elif line.strip():
previous = line
elif line.strip() == "```":
blocks.append((current, mark))
current = None
else:
current.append(line)
return blocks
def slide_blocks(environments):
text = SLIDES.read_text(encoding="utf-8")
pattern = r"\\begin\{(?:%s)\}[^\n]*\n(.*?)\\end\{(?:%s)\}" % (environments, environments)
return [match.group(1).splitlines() for match in re.finditer(pattern, text, re.S)]
def known_code_lines():
lines = set()
sources = list(HERE.glob("*.c")) + list(HERE.glob("*.h")) + list((HERE / "errors").glob("*.c"))
sources += list((HERE / "lab").glob("*.c")) + list((HERE / "lab").glob("*.h"))
for source in sources:
lines.update(line.strip() for line in source.read_text(encoding="utf-8").splitlines())
return lines
def stable_report_line(line):
line = normalize(line).rstrip()
line = line.replace(str(HERE.resolve()) + "/", "")
line = re.sub(r"0x[0-9a-fA-F]+", "ADDR", line)
line = re.sub(r"==\d+==", "==N==", line)
return line.strip()
def assert_in_order(case, wanted, got, label):
position = 0
for line in wanted:
if not line.strip() or line.strip() == "...":
continue
with case.subTest(label=label, line=line):
found = [i for i in range(position, len(got)) if line in got[i]]
case.assertTrue(found, f"no aparece en la salida real: {line}")
if found:
position = found[0] + 1
class NotesAreTrue(unittest.TestCase):
@unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
def test_terminal_blocks_in_notes(self):
checked = 0
for lines, mark in note_blocks("text"):
if not lines or not lines[0].startswith("$ ") or mark == LAB_OUTPUT:
continue
for command, expected in split_commands(lines):
actual = clean_lines(run_shell(command))
checked += 1
if mark == REPORT:
assert_in_order(self, [stable_report_line(line) for line in expected],
[stable_report_line(line) for line in actual], command)
elif mark != SYSTEM_DEPENDENT:
with self.subTest(command=command):
self.assertEqual(actual, expected)
self.assertGreater(checked, 10)
@unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
def test_code_blocks_in_notes_exist(self):
known = known_code_lines()
for lines, _ in note_blocks("c"):
for line in lines:
if line.strip() and line.strip() != "...":
with self.subTest(line=line):
self.assertIn(line.strip(), known)
@unittest.skipUnless(NOTES.exists() and (HERE / "solution").exists(), "falta la solución")
def test_lab_output_in_notes(self):
checked = 0
for lines, mark in note_blocks("text"):
if mark == LAB_OUTPUT:
command = lines[0][2:].replace("build/lab/", "build/solution/")
actual = clean_lines(run_shell(command))
self.assertEqual(actual, [line.rstrip() for line in lines[1:]])
checked += 1
self.assertGreater(checked, 0)
class SlidesAreTrue(unittest.TestCase):
@unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
def test_terminal_blocks_in_slides(self):
# Los informes van en otro entorno, informe, que no se compara.
for block in slide_blocks("terminal|salida|salidalarga"):
for command, expected in split_commands(block):
actual = clean_lines(run_shell(command))
position = 0
# Una diapositiva puede omitir líneas, pero no cambiarlas ni reordenarlas.
for line in expected:
line = line.strip()
if not line:
continue
with self.subTest(command=command, line=line):
found = [i for i in range(position, len(actual)) if actual[i].strip() == line]
self.assertTrue(found, f"no aparece en la salida real: {line}")
if found:
position = found[0] + 1
@unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
def test_code_blocks_in_slides_exist(self):
known = known_code_lines()
for block in slide_blocks("codigo|codigochico|codigomini"):
for line in block:
if line.strip() and line.strip() != "...":
with self.subTest(line=line):
self.assertIn(line.strip(), known)
if __name__ == "__main__":
unittest.main()Laboratorio
Archivos de partida del laboratorio de la semana.
#include "env.h"
#include <stdlib.h>
#include <string.h>
struct binding {
char *name;
double value;
struct binding *next;
};
struct env {
Env *parent;
struct binding *first;
};
Env *env_new(Env *parent) {
Env *env = malloc(sizeof *env);
if (env == NULL) {
return NULL;
}
env->parent = parent;
env->first = NULL;
return env;
}
void env_free(Env *env) {
if (env == NULL) {
return;
}
struct binding *current = env->first;
while (current != NULL) {
struct binding *next = current->next;
free(current->name);
free(current);
current = next;
}
free(env);
}
int env_set(Env *env, const char *name, double value) {
for (struct binding *b = env->first; b != NULL; b = b->next) {
if (strcmp(b->name, name) == 0) {
b->value = value;
return 1;
}
}
struct binding *b = malloc(sizeof *b);
if (b == NULL) {
return 0;
}
b->name = malloc(strlen(name) + 1);
if (b->name == NULL) {
free(b);
return 0;
}
strcpy(b->name, name);
b->value = value;
b->next = env->first;
env->first = b;
return 1;
}
int env_get(const Env *env, const char *name, double *value) {
for (const Env *e = env; e != NULL; e = e->parent) {
for (const struct binding *b = e->first; b != NULL; b = b->next) {
if (strcmp(b->name, name) == 0) {
*value = b->value;
return 1;
}
}
}
return 0;
}// Tabla de variables de un marco. Se usa solo por esta interfaz.
//
// Owner: quien llama a env_new libera con env_free. env_free no libera al padre.
#ifndef ENV_H
#define ENV_H
typedef struct env Env;
// Crea un marco vacío. parent puede ser NULL. Devuelve NULL si no hay memoria.
Env *env_new(Env *parent);
// Libera el marco y sus nombres. Acepta NULL.
void env_free(Env *env);
// Define name en este marco, o cambia su valor si ya está en este marco.
// Copia el nombre. Devuelve 1 si pudo y 0 si no hay memoria.
int env_set(Env *env, const char *name, double value);
// Busca name en este marco y luego en sus padres. Si lo encuentra, guarda el
// valor en *value y devuelve 1. Si no, devuelve 0 y no toca *value.
int env_get(const Env *env, const char *name, double *value);
#endif#include "expr.h"
#include <stdlib.h>
#include <string.h>
static Expr *expr_node(ExprKind kind) {
Expr *e = malloc(sizeof *e);
if (e == NULL) {
return NULL;
}
e->kind = kind;
return e;
}
Expr *expr_number(double value) {
Expr *e = expr_node(EXPR_NUMBER);
if (e == NULL) {
return NULL;
}
e->as.number = value;
return e;
}
Expr *expr_variable(const char *name) {
Expr *e = expr_node(EXPR_VARIABLE);
if (e == NULL) {
return NULL;
}
e->as.name = malloc(strlen(name) + 1);
if (e->as.name == NULL) {
free(e);
return NULL;
}
strcpy(e->as.name, name);
return e;
}
Expr *expr_binary(BinaryOp op, Expr *left, Expr *right) {
if (left == NULL || right == NULL) {
expr_free(left);
expr_free(right);
return NULL;
}
Expr *e = expr_node(EXPR_BINARY);
if (e == NULL) {
expr_free(left);
expr_free(right);
return NULL;
}
e->as.binary.op = op;
e->as.binary.left = left;
e->as.binary.right = right;
return e;
}
void expr_free(Expr *e) {
if (e == NULL) {
return;
}
switch (e->kind) {
case EXPR_NUMBER:
break;
case EXPR_VARIABLE:
free(e->as.name);
break;
case EXPR_BINARY:
expr_free(e->as.binary.left);
expr_free(e->as.binary.right);
break;
}
free(e);
}
// El nivel de las comparaciones, el más bajo.
enum { COMPARISON = 1 };
// Mayor número, liga más fuerte. Las hojas nunca necesitan paréntesis.
static int precedence(const Expr *e) {
if (e->kind != EXPR_BINARY) {
return 4;
}
switch (e->as.binary.op) {
case OP_MUL:
case OP_DIV:
return 3;
case OP_ADD:
case OP_SUB:
return 2;
case OP_LESS:
case OP_LESS_EQUAL:
case OP_GREATER:
case OP_GREATER_EQUAL:
case OP_EQUAL:
case OP_NOT_EQUAL:
break;
}
return COMPARISON;
}
static const char *op_symbol(BinaryOp op) {
switch (op) {
case OP_ADD:
return "+";
case OP_SUB:
return "-";
case OP_MUL:
return "*";
case OP_DIV:
return "/";
case OP_LESS:
return "<";
case OP_LESS_EQUAL:
return "<=";
case OP_GREATER:
return ">";
case OP_GREATER_EQUAL:
return ">=";
case OP_EQUAL:
return "==";
case OP_NOT_EQUAL:
return "!=";
}
return "?";
}
static void print_child(const Expr *child, int needs_parens, FILE *out) {
if (needs_parens) {
fputc('(', out);
}
expr_print(child, out);
if (needs_parens) {
fputc(')', out);
}
}
void expr_print(const Expr *e, FILE *out) {
switch (e->kind) {
case EXPR_NUMBER:
fprintf(out, "%g", e->as.number);
break;
case EXPR_VARIABLE:
fputs(e->as.name, out);
break;
case EXPR_BINARY: {
const Expr *left = e->as.binary.left;
const Expr *right = e->as.binary.right;
// Las comparaciones no se encadenan: a la izquierda, el mismo nivel también lleva paréntesis.
int level = precedence(e);
print_child(left, precedence(left) < level || (level == COMPARISON && precedence(left) == level), out);
fprintf(out, " %s ", op_symbol(e->as.binary.op));
print_child(right, precedence(right) <= level, out);
break;
}
}
}
// Aplica op a dos números. Devuelve 0 si es una división entre 0.
static int apply_op(BinaryOp op, double left, double right, double *result) {
switch (op) {
case OP_ADD:
*result = left + right;
return 1;
case OP_SUB:
*result = left - right;
return 1;
case OP_MUL:
*result = left * right;
return 1;
case OP_DIV:
if (right == 0.0) {
return 0;
}
*result = left / right;
return 1;
case OP_LESS:
*result = left < right;
return 1;
case OP_LESS_EQUAL:
*result = left <= right;
return 1;
case OP_GREATER:
*result = left > right;
return 1;
case OP_GREATER_EQUAL:
*result = left >= right;
return 1;
case OP_EQUAL:
*result = left == right;
return 1;
case OP_NOT_EQUAL:
*result = left != right;
return 1;
}
return 0;
}
int expr_eval(const Expr *e, const Env *env, double *result) {
switch (e->kind) {
case EXPR_NUMBER:
*result = e->as.number;
return 1;
case EXPR_VARIABLE:
return env_get(env, e->as.name, result);
case EXPR_BINARY: {
double left;
double right;
if (!expr_eval(e->as.binary.left, env, &left)) {
return 0;
}
if (!expr_eval(e->as.binary.right, env, &right)) {
return 0;
}
return apply_op(e->as.binary.op, left, right, result);
}
}
return 0;
}
Expr *expr_copy(const Expr *e) {
switch (e->kind) {
case EXPR_NUMBER:
return expr_number(e->as.number);
case EXPR_VARIABLE:
return expr_variable(e->as.name);
case EXPR_BINARY:
return expr_binary(e->as.binary.op, expr_copy(e->as.binary.left), expr_copy(e->as.binary.right));
}
return NULL;
}
int expr_equal(const Expr *a, const Expr *b) {
if (a->kind != b->kind) {
return 0;
}
switch (a->kind) {
case EXPR_NUMBER:
return a->as.number == b->as.number;
case EXPR_VARIABLE:
return strcmp(a->as.name, b->as.name) == 0;
case EXPR_BINARY:
return a->as.binary.op == b->as.binary.op
&& expr_equal(a->as.binary.left, b->as.binary.left)
&& expr_equal(a->as.binary.right, b->as.binary.right);
}
return 0;
}
static int is_number(const Expr *e, double value) {
return e->kind == EXPR_NUMBER && e->as.number == value;
}
// Conserva un hijo, y libera al otro hijo y al nodo.
static Expr *keep_child(Expr *node, Expr *kept, Expr *dropped) {
expr_free(dropped);
free(node);
return kept;
}
// Convierte un nodo binario en un número, en el mismo lugar. No reserva nada.
static Expr *become_number(Expr *node, double value) {
expr_free(node->as.binary.left);
expr_free(node->as.binary.right);
node->kind = EXPR_NUMBER;
node->as.number = value;
return node;
}
Expr *expr_simplify(Expr *e) {
if (e == NULL || e->kind != EXPR_BINARY) {
return e;
}
e->as.binary.left = expr_simplify(e->as.binary.left);
e->as.binary.right = expr_simplify(e->as.binary.right);
Expr *left = e->as.binary.left;
Expr *right = e->as.binary.right;
BinaryOp op = e->as.binary.op;
if ((op == OP_ADD && is_number(left, 0.0)) || (op == OP_MUL && is_number(left, 1.0))) {
return keep_child(e, right, left);
}
if ((op == OP_ADD || op == OP_SUB) && is_number(right, 0.0)) {
return keep_child(e, left, right);
}
if ((op == OP_MUL || op == OP_DIV) && is_number(right, 1.0)) {
return keep_child(e, left, right);
}
if (op == OP_MUL && (is_number(left, 0.0) || is_number(right, 0.0))) {
return become_number(e, 0.0);
}
if (left->kind == EXPR_NUMBER && right->kind == EXPR_NUMBER) {
double value;
if (apply_op(op, left->as.number, right->as.number, &value)) {
return become_number(e, value);
}
}
return e;
}
Expr *expr_derive(const Expr *e, const char *name) {
switch (e->kind) {
case EXPR_NUMBER:
return expr_number(0.0);
case EXPR_VARIABLE:
return expr_number(strcmp(e->as.name, name) == 0 ? 1.0 : 0.0);
case EXPR_BINARY: {
const Expr *f = e->as.binary.left;
const Expr *g = e->as.binary.right;
switch (e->as.binary.op) {
case OP_ADD:
case OP_SUB:
return expr_binary(e->as.binary.op, expr_derive(f, name), expr_derive(g, name));
case OP_MUL:
return expr_binary(OP_ADD,
expr_binary(OP_MUL, expr_derive(f, name), expr_copy(g)),
expr_binary(OP_MUL, expr_copy(f), expr_derive(g, name)));
case OP_DIV:
return expr_binary(OP_DIV,
expr_binary(OP_SUB,
expr_binary(OP_MUL, expr_derive(f, name), expr_copy(g)),
expr_binary(OP_MUL, expr_copy(f), expr_derive(g, name))),
expr_binary(OP_MUL, expr_copy(g), expr_copy(g)));
case OP_LESS:
case OP_LESS_EQUAL:
case OP_GREATER:
case OP_GREATER_EQUAL:
case OP_EQUAL:
case OP_NOT_EQUAL:
return NULL;
}
return NULL;
}
}
return NULL;
}// Árboles de expresión: el módulo de la semana 13, terminado, con las comparaciones.
//
// Expr no es opaco: el código mira kind y usa un switch, como en la semana 7.
//
// Ownership: cada nodo es owner de su nombre y de sus hijos. Quien recibe un árbol lo libera
// con expr_free. Los nodos no se comparten: cada hijo tiene un solo padre.
#ifndef EXPR_H
#define EXPR_H
#include <stdio.h>
#include "env.h"
typedef enum { EXPR_NUMBER, EXPR_VARIABLE, EXPR_BINARY } ExprKind;
typedef enum {
OP_ADD, OP_SUB, OP_MUL, OP_DIV,
OP_LESS, OP_LESS_EQUAL, OP_GREATER, OP_GREATER_EQUAL, OP_EQUAL, OP_NOT_EQUAL
} BinaryOp;
typedef struct expr Expr;
struct expr {
ExprKind kind;
union {
double number; // EXPR_NUMBER
char *name; // EXPR_VARIABLE: el nodo es owner de su copia
struct { BinaryOp op; Expr *left; Expr *right; } binary; // EXPR_BINARY: el nodo es owner de sus hijos
} as;
};
// Devuelve un nodo nuevo, o NULL si no hay memoria.
Expr *expr_number(double value);
// Guarda una copia de name. Devuelve NULL si no hay memoria, sin dejar nada reservado.
Expr *expr_variable(const char *name);
// Se queda con los hijos, también cuando falla. Si no hay memoria, o si un hijo es NULL,
// libera los hijos que tenga y devuelve NULL. Por eso se pueden anidar llamadas sin revisar
// cada una: basta con revisar el resultado final.
Expr *expr_binary(BinaryOp op, Expr *left, Expr *right);
// Libera el árbol completo. Acepta NULL.
void expr_free(Expr *e);
// Escribe la expresión en out, con los números en %g y un espacio a cada lado del operador,
// sin salto de línea al final. Solo pone los paréntesis necesarios para conservar la forma del
// árbol, sabiendo que * y / van antes que + y -, que estos van antes que las comparaciones, y que
// + - * / asocian por la izquierda:
// - un hijo izquierdo lleva paréntesis si su operador tiene menor precedencia que el del padre;
// - un hijo derecho lleva paréntesis si su operador tiene menor o igual precedencia;
// - las comparaciones no se encadenan: una comparación dentro de otra lleva paréntesis a los dos lados.
// Ejemplos: 2 + 3 * x (2 + 3) * x 1 - (2 - 3) 1 + (2 + 3) x + 1 < y (1 < 2) == 1
void expr_print(const Expr *e, FILE *out);
// Calcula el valor de e. Si puede, lo guarda en *result y devuelve 1. Si hay una variable que
// no existe en env, o una división entre un divisor igual a 0, devuelve 0 y no toca *result.
// Una comparación vale 1 o 0.
int expr_eval(const Expr *e, const Env *env, double *result);
// Devuelve una copia independiente del árbol, que no comparte nada con e. NULL si no hay
// memoria, sin dejar nada reservado.
Expr *expr_copy(const Expr *e);
// Devuelve 1 si los dos árboles tienen la misma forma y los mismos valores, y 0 si no.
int expr_equal(const Expr *a, const Expr *b);
// Se queda con e y devuelve el árbol simplificado. Libera todo lo que ya no se usa. Simplifica
// primero a los hijos, y luego aplica a cada nodo estas reglas:
// x + 0, 0 + x, x - 0, x * 1, 1 * x, x / 1 -> x
// x * 0, 0 * x -> 0
// dos números -> el resultado, salvo si es una división entre 0
// De las comparaciones, solo se calcula la de dos números.
// Con NULL devuelve NULL.
Expr *expr_simplify(Expr *e);
// Devuelve un árbol nuevo con la derivada de e respecto de la variable name, o NULL si no hay
// memoria o si e contiene una comparación, que no se deriva. No deja nada reservado. No cambia e
// ni comparte nodos con él.
Expr *expr_derive(const Expr *e, const char *name);
#endif// Pruebas del analizador léxico. Si algo falla, assert dice en qué línea.
#include <assert.h>
#include <string.h>
#include "lexer.h"
// Comprueba que el texto produzca esos tipos de token, y luego TOKEN_END.
static void check_kinds(const char *source, const TokenKind *wanted, int count) {
Lexer *lexer = lexer_new(source);
assert(lexer != NULL);
for (int i = 0; i < count; i++) {
assert(lexer_next(lexer).kind == wanted[i]);
}
assert(lexer_next(lexer).kind == TOKEN_END);
lexer_free(lexer);
}
static void test_symbols_and_names(void) {
const TokenKind wanted[] = {TOKEN_LET, TOKEN_NAME, TOKEN_ASSIGN, TOKEN_NUMBER, TOKEN_STAR, TOKEN_LPAREN,
TOKEN_NAME, TOKEN_PLUS, TOKEN_NUMBER, TOKEN_RPAREN};
check_kinds("let x1 = 3*(y+2)", wanted, 10);
}
static void test_a_keyword_needs_the_whole_word(void) {
const TokenKind wanted[] = {TOKEN_NAME, TOKEN_NAME, TOKEN_IF};
check_kinds("lets _if if", wanted, 3);
}
static void test_number_value_and_text(void) {
Lexer *lexer = lexer_new("3.25");
assert(lexer != NULL);
Token token = lexer_next(lexer);
assert(token.kind == TOKEN_NUMBER);
assert(token.number == 3.25);
assert(strcmp(token.text, "3.25") == 0);
lexer_free(lexer);
}
static void test_positions(void) {
Lexer *lexer = lexer_new("x\n +");
assert(lexer != NULL);
Token first = lexer_next(lexer);
assert(first.line == 1 && first.column == 1);
Token second = lexer_next(lexer);
assert(second.line == 2 && second.column == 3);
lexer_free(lexer);
}
static void test_unknown_character(void) {
Lexer *lexer = lexer_new("@ 1");
assert(lexer != NULL);
assert(lexer_next(lexer).kind == TOKEN_ERROR);
assert(lexer_next(lexer).kind == TOKEN_NUMBER);
lexer_free(lexer);
}
// Parte 1: agreguen aquí sus pruebas de los comparadores y de los comentarios,
// y llámenlas desde main.
int main(void) {
test_symbols_and_names();
test_a_keyword_needs_the_whole_word();
test_number_value_and_text();
test_positions();
test_unknown_character();
return 0;
}#include "lexer.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct lexer {
const char *current; // el siguiente carácter por leer
int line;
int column;
};
Lexer *lexer_new(const char *source) {
Lexer *lexer = malloc(sizeof *lexer);
if (lexer == NULL) {
return NULL;
}
lexer->current = source;
lexer->line = 1;
lexer->column = 1;
return lexer;
}
void lexer_free(Lexer *lexer) {
free(lexer);
}
// Consume un carácter y lleva la cuenta de la línea y la columna.
static void advance(Lexer *lexer) {
if (*lexer->current == '\n') {
lexer->line++;
lexer->column = 1;
} else {
lexer->column++;
}
lexer->current++;
}
// Salta lo que separa a los tokens.
// Laboratorio, parte 1: aquí también se saltan los comentarios.
static void skip_blanks(Lexer *lexer) {
while (isspace((unsigned char) *lexer->current)) {
advance(lexer);
}
}
static int starts_name(char c) {
return isalpha((unsigned char) c) || c == '_';
}
static int continues_name(char c) {
return isalnum((unsigned char) c) || c == '_';
}
static int is_digit(char c) {
return isdigit((unsigned char) c);
}
// Un token que ya se reconoció: guarda su tipo y su texto.
static Token finish(Token token, TokenKind kind, const char *text) {
token.kind = kind;
snprintf(token.text, sizeof token.text, "%s", text);
return token;
}
// Lee desde start hasta el lexer actual y lo copia a token->text.
// Devuelve 0 si no cabe. Mide primero: no se escribe nada fuera del arreglo.
static int copy_text(Token *token, const char *start, const Lexer *lexer) {
size_t length = (size_t) (lexer->current - start);
if (length >= sizeof token->text) {
return 0;
}
memcpy(token->text, start, length);
token->text[length] = '\0';
return 1;
}
// NUMBER: dígitos y una parte decimal opcional. El punto solo entra si le sigue un dígito.
static Token read_number(Lexer *lexer, Token token) {
const char *start = lexer->current;
while (is_digit(*lexer->current)) {
advance(lexer);
}
if (lexer->current[0] == '.' && is_digit(lexer->current[1])) {
advance(lexer);
while (is_digit(*lexer->current)) {
advance(lexer);
}
}
if (!copy_text(&token, start, lexer)) {
return finish(token, TOKEN_ERROR, "número demasiado largo");
}
token.kind = TOKEN_NUMBER;
token.number = strtod(token.text, NULL);
return token;
}
// Una palabra reservada, o TOKEN_NAME si no es ninguna.
static TokenKind keyword_or_name(const char *text) {
static const struct {
char word[8];
TokenKind kind;
} keywords[] = {
{"let", TOKEN_LET}, {"print", TOKEN_PRINT}, {"def", TOKEN_DEF},
{"if", TOKEN_IF}, {"then", TOKEN_THEN}, {"else", TOKEN_ELSE},
};
size_t count = sizeof keywords / sizeof keywords[0];
for (size_t i = 0; i < count; i++) {
if (strcmp(text, keywords[i].word) == 0) {
return keywords[i].kind;
}
}
return TOKEN_NAME;
}
// NAME: letra o guion bajo, y luego letras, dígitos o guiones bajos.
static Token read_name(Lexer *lexer, Token token) {
const char *start = lexer->current;
while (continues_name(*lexer->current)) {
advance(lexer);
}
if (!copy_text(&token, start, lexer)) {
return finish(token, TOKEN_ERROR, "nombre demasiado largo");
}
token.kind = keyword_or_name(token.text);
return token;
}
// Un carácter que no empieza ningún token. Lo consume, para que se pueda seguir.
static Token unexpected(Lexer *lexer, Token token) {
unsigned char c = (unsigned char) *lexer->current;
advance(lexer);
token.kind = TOKEN_ERROR;
if (isprint(c)) {
snprintf(token.text, sizeof token.text, "carácter inesperado '%c'", c);
} else {
snprintf(token.text, sizeof token.text, "carácter inesperado (byte %d)", c);
}
return token;
}
Token lexer_next(Lexer *lexer) {
skip_blanks(lexer);
Token token = {.kind = TOKEN_END, .number = 0.0, .text = "", .line = lexer->line, .column = lexer->column};
char c = *lexer->current;
if (c == '\0') {
return token;
}
if (is_digit(c)) {
return read_number(lexer, token);
}
if (starts_name(c)) {
return read_name(lexer, token);
}
// Laboratorio, parte 1: aquí faltan los comparadores.
switch (c) {
case '+': advance(lexer); return finish(token, TOKEN_PLUS, "+");
case '-': advance(lexer); return finish(token, TOKEN_MINUS, "-");
case '*': advance(lexer); return finish(token, TOKEN_STAR, "*");
case '/': advance(lexer); return finish(token, TOKEN_SLASH, "/");
case '(': advance(lexer); return finish(token, TOKEN_LPAREN, "(");
case ')': advance(lexer); return finish(token, TOKEN_RPAREN, ")");
case ',': advance(lexer); return finish(token, TOKEN_COMMA, ",");
case '=': advance(lexer); return finish(token, TOKEN_ASSIGN, "=");
default: return unexpected(lexer, token);
}
}// Análisis léxico del lenguaje Mini: parte un texto en tokens, uno por llamada.
//
// Owner: quien llama a lexer_new libera con lexer_free. El lexer no copia el texto:
// source debe seguir vivo mientras se use el lexer.
#ifndef LEXER_H
#define LEXER_H
typedef enum {
TOKEN_NUMBER, TOKEN_NAME,
TOKEN_LET, TOKEN_PRINT, TOKEN_DEF, TOKEN_IF, TOKEN_THEN, TOKEN_ELSE,
TOKEN_PLUS, TOKEN_MINUS, TOKEN_STAR, TOKEN_SLASH,
TOKEN_LPAREN, TOKEN_RPAREN, TOKEN_COMMA, TOKEN_ASSIGN,
TOKEN_LESS, TOKEN_LESS_EQUAL, TOKEN_GREATER, TOKEN_GREATER_EQUAL, TOKEN_EQUAL, TOKEN_NOT_EQUAL,
TOKEN_END, TOKEN_ERROR
} TokenKind;
// line y column empiezan en 1 y dicen dónde empieza el token. column cuenta bytes.
// text es el texto del token. En un TOKEN_ERROR, text es el mensaje de error.
// number solo tiene sentido en un TOKEN_NUMBER.
typedef struct {
TokenKind kind;
double number;
char text[64];
int line;
int column;
} Token;
typedef struct lexer Lexer;
// Devuelve NULL si no hay memoria.
Lexer *lexer_new(const char *source);
// Acepta NULL.
void lexer_free(Lexer *lexer);
// Devuelve el token siguiente. Al llegar al final devuelve TOKEN_END, y lo sigue
// devolviendo si se le vuelve a llamar. Después de un TOKEN_ERROR se puede seguir.
Token lexer_next(Lexer *lexer);
#endif// Analiza una expresión, escribe el árbol y lo evalúa con x = 2 e y = 3.
// Si hay un error de sintaxis, muestra la línea y marca la columna.
#include <stdio.h>
#include "env.h"
#include "expr.h"
#include "parser.h"
// Escribe la línea número line de source, y debajo una marca en la columna.
static void show_position(const char *source, int line, int column) {
const char *start = source;
for (int current = 1; current < line && *start != '\0'; start++) {
if (*start == '\n') {
current++;
}
}
for (const char *c = start; *c != '\0' && *c != '\n'; c++) {
putchar(*c);
}
putchar('\n');
for (int i = 1; i < column; i++) {
putchar(' ');
}
puts("^");
}
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "uso: %s 'expresión'\n", argv[0]);
return 1;
}
ParseError error;
Expr *tree = parse_expression(argv[1], &error);
if (tree == NULL) {
show_position(argv[1], error.line, error.column);
printf("línea %d, columna %d: %s\n", error.line, error.column, error.message);
return 1;
}
printf("árbol: ");
expr_print(tree, stdout);
putchar('\n');
int status = 1;
Env *env = env_new(NULL);
if (env != NULL && env_set(env, "x", 2.0) && env_set(env, "y", 3.0)) {
double value;
if (expr_eval(tree, env, &value)) {
printf("valor: %g\n", value);
} else {
puts("no se pudo evaluar: variable desconocida o división entre cero");
}
status = 0;
}
env_free(env);
expr_free(tree);
return status;
}// Parser descendente recursivo: una función por cada regla de la gramática.
//
// expr → compare
// compare → sum [ ("<" | "<=" | ">" | ">=" | "==" | "!=") sum ]
// sum → term (("+" | "-") term)*
// term → unary (("*" | "/") unary)*
// unary → "-" unary | primary
// primary → NUMBER | NAME | "(" expr ")"
//
// Cada parse_* mira p->current, consume lo que le toca y devuelve un árbol del que quien
// llama es owner. Si falla, deja el error llenado, libera lo suyo y devuelve NULL.
#include "parser.h"
#include <stdio.h>
#include <stdlib.h>
#include "lexer.h"
typedef struct {
Lexer *lexer;
Token current; // el token que toca mirar, todavía sin consumir
ParseError *error;
} Parser;
static Expr *parse_expr(Parser *p);
// Pasa al token siguiente.
static void advance(Parser *p) {
p->current = lexer_next(p->lexer);
}
// Por escribir (parte 3): anota el error en p->error, con la línea y la columna de token.
// Si token es un TOKEN_ERROR, el mensaje es token.text: el analizador léxico ya lo explicó.
// Si no, es message. Copien con snprintf.
static void syntax_error(Parser *p, Token token, const char *message) {
(void) p;
(void) token;
(void) message;
}
// Por escribir (parte 2): NUMBER | NAME | "(" expr ")"
// Cualquier otro token es un error, y aquí se reporta con syntax_error.
static Expr *parse_primary(Parser *p) {
syntax_error(p, p->current, "se esperaba un número, un nombre o '('");
return NULL;
}
// Por escribir (parte 2): "-" unary | primary
// -x se construye como 0 - x: no hay una variante nueva en Expr.
static Expr *parse_unary(Parser *p) {
return parse_primary(p);
}
// Por escribir (parte 2): unary (("*" | "/") unary)*
static Expr *parse_term(Parser *p) {
return parse_unary(p);
}
// Por escribir (parte 2): term (("+" | "-") term)*
static Expr *parse_sum(Parser *p) {
return parse_term(p);
}
// Por escribir (parte 2): sum [ ("<" | "<=" | ">" | ">=" | "==" | "!=") sum ]
// Hay un comparador como máximo. Sin ciclo: 1 < 2 < 3 es un error.
static Expr *parse_compare(Parser *p) {
return parse_sum(p);
}
// Ya está hecha. La próxima semana, aquí entra if.
static Expr *parse_expr(Parser *p) {
return parse_compare(p);
}
// Ya está hecha.
Expr *parse_expression(const char *source, ParseError *error) {
error->line = 0;
error->column = 0;
error->message[0] = '\0';
Parser parser = {.lexer = lexer_new(source), .error = error};
if (parser.lexer == NULL) {
snprintf(error->message, sizeof error->message, "no hay memoria");
return NULL;
}
advance(&parser);
Expr *tree = parse_expr(&parser);
if (tree != NULL && parser.current.kind != TOKEN_END) {
syntax_error(&parser, parser.current, "sobra texto después de la expresión");
expr_free(tree);
tree = NULL;
}
lexer_free(parser.lexer);
return tree;
}// Análisis sintáctico de expresiones de Mini. Las instrucciones, if y las llamadas
// llegan la próxima semana.
#ifndef PARSER_H
#define PARSER_H
#include "expr.h"
// line y column dicen dónde está el token con el que falló.
typedef struct {
int line;
int column;
char message[128];
} ParseError;
// Devuelve el árbol, y quien llama es su owner. Si hay un error de sintaxis devuelve
// NULL, llena *error con el primer error y no deja nada sin liberar.
// source debe ser una sola expresión completa: si sobra texto, es un error.
Expr *parse_expression(const char *source, ParseError *error);
#endif# Laboratorio de la semana 14: parser de expresiones de Mini
Esta semana hay **una sola sesión** de laboratorio, pensada para unos 90 minutos de trabajo. La otra es para la Práctica 4.
Los comandos se corren desde `code/week-14`. Las ideas están en los apuntes, en la sección "El laboratorio, paso a paso".
| Archivo | Qué es | ¿Lo cambian? |
|---|---|---|
| `expr.h`, `expr.c` | El módulo `Expr` de la semana 13, terminado, con las comparaciones agregadas | No |
| `env.h`, `env.c` | La tabla de variables | No |
| `lexer.h` | La interfaz del analizador léxico: `TokenKind`, `Token` y tres funciones | No |
| `lexer.c` | El analizador léxico, sin comparadores ni comentarios | **Sí** |
| `lexer_tests.c` | Sus pruebas con `assert` | **Sí** |
| `show_tokens.c` | Escribe los tokens de un texto | No |
| `parser.h` | La interfaz: `ParseError` y `parse_expression` | No |
| `parser.c` | El parser. `advance`, `parse_expr` y `parse_expression` ya están hechas | **Sí** |
| `parse_main.c` | Analiza una expresión, escribe el árbol y lo evalúa con `x` = 2 e `y` = 3 | No |
```sh
make lab # compila todo en build/lab/
make test-lab # comprueba todo el laboratorio; falla hasta que terminen
```
`make test-lab` agrupa las pruebas por parte. Se puede correr un grupo solo:
```sh
LAB_DIR=lab python3 -m unittest test_lab.Lexer
```
Todo se compila con ASan y UBSan. Una fuga hace fallar la prueba, aunque el resultado sea correcto.
## Parte 1: el analizador léxico (unos 25 minutos)
Lean `lexer.h` y `lexer.c` completos, y luego `lexer_tests.c`. Respondan por escrito, sin ejecutar nada:
1. ¿Por qué `lexer_new` pide que el texto siga vivo? ¿Qué pasaría si se libera antes que el lexer?
2. ¿Qué tokens produce `1.5.2`? ¿Y `12abc`? Compruébenlo con `build/lab/show_tokens`.
3. ¿Qué devuelve `lexer_next` con un nombre de 100 letras? ¿Por qué no se escribe fuera de `text`?
Luego, en `lexer.c`:
1. Agreguen los comparadores `<`, `<=`, `>`, `>=`, `==` y `!=`. Hay que mirar un carácter más, y `=` ya no siempre es
`TOKEN_ASSIGN`. Un `!` solo es un error.
2. Hagan que `skip_blanks` salte los comentarios: desde `#` hasta el final de la línea. El salto de línea no es parte del
comentario, y una línea debe seguir contándose.
3. En `lexer_tests.c`, agreguen una prueba de los comparadores y otra de los comentarios.
```sh
make lab
build/lab/lexer_tests
build/lab/show_tokens 'x <= 1 # hola'
LAB_DIR=lab python3 -m unittest test_lab.Lexer
```
## Parte 2: el parser (unos 45 minutos)
Lean `parser.h` y `parser.c`. Las funciones `parse_*` están encadenadas, pero solo pasan el trabajo a la siguiente.
Escríbanlas en este orden, y prueben después de cada una:
1. `parse_primary`: un número, un nombre o una expresión entre paréntesis. Ya se puede probar `42`, `x` y `((x))`.
2. `parse_term` y `parse_sum`: con un ciclo. Prueben `1 + 2 * 3` y `1 - 2 - 3`.
3. `parse_unary`: `-x` se construye como `0 - x`.
4. `parse_compare`: un comparador como máximo. Por eso no lleva ciclo.
Cada función libera lo suyo antes de devolver `NULL`, y revisa lo que devuelven los constructores. Háganlo desde el
inicio, no al final. `expr_binary` toma posesión de sus hijos incluso si falla. Los apuntes tienen el mismo patrón en
`logic_parser.c`.
```sh
make lab
build/lab/parse_main '1 + 2 * 3'
build/lab/parse_main '(1 + 2) * x < -y'
LAB_DIR=lab python3 -m unittest test_lab.Trees
```
## Parte 3: errores (unos 20 minutos)
1. Escriban `syntax_error`: la línea y la columna del token, y el mensaje. Si el token es `TOKEN_ERROR`, el mensaje es el
que trae el token.
2. Prueben entradas malas, y digan antes de correr en qué columna debe marcar:
```sh
build/lab/parse_main '1 + * 2'
build/lab/parse_main '(1 + 2'
build/lab/parse_main '1 < 2 < 3'
build/lab/parse_main '1 + @'
```
3. Dibujen qué está vivo cuando falla `1 + 2 * (3 +`. ¿Quién libera cada nodo?
4. Corran todo:
```sh
make test-lab
```
Una prueba hace que cada `malloc`, uno por uno, devuelva `NULL`. Si falla, revisen qué pasa cuando un constructor devuelve
`NULL`: no es un error de sintaxis, pero también hay que llenar el mensaje.
**Si no alcanza el tiempo:** lo que falte lo terminan antes de la próxima clase. La semana 15 trae el parser hecho, pero
agrega `if`, llamadas e instrucciones sobre esta misma forma, así que conviene llegar con esto claro.
## Si sobra tiempo
1. Agreguen a `parse_main` un modo que escriba el árbol con sangría, un nodo por línea.
2. Prueben `build/lab/parse_main` con 100000 paréntesis abiertos. ¿Qué pasa, y por qué? Propongan un arreglo sin escribirlo.
3. ¿Qué tendría que cambiar para que `2 ^ 3 ^ 2` existiera y asociara por la derecha? Escriban primero la regla.// Escribe los tokens de un texto, uno por línea: línea:columna, tipo y texto.
#include <stdio.h>
#include "lexer.h"
static const char *kind_name(TokenKind kind) {
switch (kind) {
case TOKEN_NUMBER: return "NUMBER";
case TOKEN_NAME: return "NAME";
case TOKEN_LET: return "LET";
case TOKEN_PRINT: return "PRINT";
case TOKEN_DEF: return "DEF";
case TOKEN_IF: return "IF";
case TOKEN_THEN: return "THEN";
case TOKEN_ELSE: return "ELSE";
case TOKEN_PLUS: return "PLUS";
case TOKEN_MINUS: return "MINUS";
case TOKEN_STAR: return "STAR";
case TOKEN_SLASH: return "SLASH";
case TOKEN_LPAREN: return "LPAREN";
case TOKEN_RPAREN: return "RPAREN";
case TOKEN_COMMA: return "COMMA";
case TOKEN_ASSIGN: return "ASSIGN";
case TOKEN_LESS: return "LESS";
case TOKEN_LESS_EQUAL: return "LESS_EQUAL";
case TOKEN_GREATER: return "GREATER";
case TOKEN_GREATER_EQUAL: return "GREATER_EQUAL";
case TOKEN_EQUAL: return "EQUAL";
case TOKEN_NOT_EQUAL: return "NOT_EQUAL";
case TOKEN_END: return "END";
case TOKEN_ERROR: return "ERROR";
}
return "?";
}
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "uso: %s 'texto'\n", argv[0]);
return 1;
}
Lexer *lexer = lexer_new(argv[1]);
if (lexer == NULL) {
fprintf(stderr, "no hay memoria\n");
return 1;
}
Token token;
do {
token = lexer_next(lexer);
printf("%d:%-3d %-14s %s\n", token.line, token.column, kind_name(token.kind), token.text);
} while (token.kind != TOKEN_END);
lexer_free(lexer);
return 0;
}Compilación
Cómo se compilan y ejecutan los ejemplos.
# Semana 14: análisis léxico y sintáctico. Se usa dentro del entorno del curso.
#
# make compila los programas de la clase en build/
# make lab compila el laboratorio en build/lab/
# make test corre las pruebas de los ejemplos y de los apuntes
# make test-lab prueba su laboratorio; falla hasta que lo terminen
# make clean borra lo generado
#
# Los programas de errors/ tienen errores a propósito. No se compilan con make:
# los compilan las pruebas, y ustedes, con los comandos de los apuntes.
CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -g -fsanitize=address,undefined
BUILD = build
PROGRAMS = logic_parser subtract_chain
LAB_HEADERS = lab/expr.h lab/env.h lab/lexer.h lab/parser.h
LAB_PROGRAMS = show_tokens lexer_tests parse_main
.PHONY: all lab solution test test-lab clean
all: $(PROGRAMS:%=$(BUILD)/%)
# Los programas de la clase usan el analizador léxico de lab/.
$(BUILD)/%: %.c lab/lexer.c lab/lexer.h | $(BUILD)
$(CC) $(CFLAGS) -Ilab $< lab/lexer.c -o $@
# Cada .c se compila por separado y luego se enlaza, como en la semana 6.
$(BUILD)/lab/%.o: lab/%.c $(LAB_HEADERS) | $(BUILD)/lab
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD)/lab/show_tokens: $(BUILD)/lab/show_tokens.o $(BUILD)/lab/lexer.o
$(CC) $(CFLAGS) $^ -o $@
$(BUILD)/lab/lexer_tests: $(BUILD)/lab/lexer_tests.o $(BUILD)/lab/lexer.o
$(CC) $(CFLAGS) $^ -o $@
$(BUILD)/lab/parse_main: $(BUILD)/lab/parse_main.o $(BUILD)/lab/parser.o $(BUILD)/lab/lexer.o $(BUILD)/lab/expr.o $(BUILD)/lab/env.o
$(CC) $(CFLAGS) $^ -o $@
# La solución trae solo lo que ustedes escriben. Lo demás se toma de lab/.
$(BUILD)/solution/%.o: solution/%.c $(LAB_HEADERS) | $(BUILD)/solution
$(CC) $(CFLAGS) -Ilab -c $< -o $@
$(BUILD)/solution/%.o: lab/%.c $(LAB_HEADERS) | $(BUILD)/solution
$(CC) $(CFLAGS) -Ilab -c $< -o $@
$(BUILD)/solution/show_tokens: $(BUILD)/solution/show_tokens.o $(BUILD)/solution/lexer.o
$(CC) $(CFLAGS) $^ -o $@
$(BUILD)/solution/lexer_tests: $(BUILD)/solution/lexer_tests.o $(BUILD)/solution/lexer.o
$(CC) $(CFLAGS) $^ -o $@
$(BUILD)/solution/parse_main: $(BUILD)/solution/parse_main.o $(BUILD)/solution/parser.o $(BUILD)/solution/lexer.o $(BUILD)/solution/expr.o $(BUILD)/solution/env.o
$(CC) $(CFLAGS) $^ -o $@
$(BUILD) $(BUILD)/lab $(BUILD)/solution:
mkdir -p $@
lab: $(LAB_PROGRAMS:%=$(BUILD)/lab/%)
solution: $(LAB_PROGRAMS:%=$(BUILD)/solution/%)
# Si existe solution/, también se comprueba la solución del laboratorio.
test: all lab
@if [ -d solution ]; then $(MAKE) --no-print-directory solution; fi
python3 -m unittest discover -p 'test_*.py'
test-lab: all lab
LAB_DIR=lab python3 -m unittest -v test_lab
clean:
rm -rf $(BUILD) __pycache__