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, print y llamadas a funciones.

Laboratorio — Parser del lenguaje Mini

Esta semana una sesión es para la Práctica 4.

Sesión 1:

  1. Leer el analizador léxico dado y sus pruebas, y agregarle comparadores y comentarios.
  2. Escribir el parser de expresiones con precedencia y paréntesis, que produce el Expr de la semana 13.
  3. 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

  1. ¿Qué tokens produce let x1 = 3*(y+2)?
  2. ¿Qué parte de la gramática hace que 1 + 2 * 3 valga 7?
  3. ¿Por qué una regla como expr → expr + term no se puede escribir tal cual en un parser descendente?
  4. 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.

logic_parser.cDescargar logic_parser.c
// 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;
}
subtract_chain.cDescargar subtract_chain.c
// 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.

errors/leak_on_error.cDescargar errors/leak_on_error.c
// 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;
}
errors/left_recursion.cDescargar errors/left_recursion.c
// 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.

test_examples.pyDescargar test_examples.py
"""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()
test_lab.pyDescargar test_lab.py
"""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()
test_notes.pyDescargar test_notes.py
"""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.

lab/env.cDescargar lab/env.c
#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;
}
lab/env.hDescargar lab/env.h
// 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
lab/expr.cDescargar lab/expr.c
#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;
}
lab/expr.hDescargar lab/expr.h
// Á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
lab/lexer_tests.cDescargar lab/lexer_tests.c
// 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;
}
lab/lexer.cDescargar lab/lexer.c
#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);
    }
}
lab/lexer.hDescargar lab/lexer.h
// 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
lab/parse_main.cDescargar lab/parse_main.c
// 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;
}
lab/parser.cDescargar lab/parser.c
// 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;
}
lab/parser.hDescargar lab/parser.h
// 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
lab/README.mdDescargar lab/README.md
# 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.
lab/show_tokens.cDescargar lab/show_tokens.c
// 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.

MakefileDescargar Makefile
# 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__