Semana 13 — Datos recursivos

Objetivos

  • Definir un tipo recursivo con una unión etiquetada y punteros.
  • Escribir funciones cuya forma sigue la forma de los datos.
  • Crear y liberar un árbol de expresión sin fugas.

Teoría — Datos recursivos: árboles de expresión, evaluar · ✂️ Simplificar

Lectura: Apuntes, semana 13.

Temas:

  • Por qué 2 + 3 * x es un árbol, y no una lista de símbolos.
  • El tipo Expr: número, variable, operación binaria. Una unión etiquetada que se apunta a sí misma.
  • La función sigue a los datos: un caso por cada variante. expr_eval, expr_print, expr_free.
  • Quién es owner de cada nodo. Liberar en el orden correcto.
  • Variables: evaluar necesita un entorno. La tabla de variables se da hecha y se usa solo por su encabezado.
  • ✂️ Simplificar: x + 0, x * 1, x * 0, y constantes. Derivar como otro recorrido.

Este árbol existe para representar expresiones. Los árboles de búsqueda y las tablas hash se implementan en Estructuras de Datos.

Laboratorio — Álgebra de expresiones

Sesión 1:

  1. Leer el encabezado dado de Expr, la tabla de variables Env y las pruebas.
  2. Escribir los constructores, expr_free y expr_print con los paréntesis necesarios.
  3. Escribir expr_eval, usando Env por su interfaz.

Sesión 2: 4. Escribir expr_copy y expr_equal. 5. Escribir expr_simplify, cuidando quién libera los nodos que ya no se usan. 6. Escribir expr_derive, y comprobar el resultado con valores numéricos.

Preguntas de repaso

  1. Dibujen el árbol de (1 + 2) * x. ¿Cuántos nodos tiene?
  2. ¿Por qué expr_free libera primero a los hijos?
  3. ¿Qué devuelve expr_eval si la variable no existe? ¿Cómo diseñarían ese error?
  4. ¿Por qué x * 0 → 0 necesita liberar el subárbol de x?

Código de la semana

Compilar y probar dentro del contenedor del curso con make y make test.

Ejemplos (7)Errores (7)Pruebas (3)Laboratorio (7)Compilación (1)

Ejemplos

Los programas que usan los apuntes y la clase.

env_demo.cDescargar env_demo.c
// Usa la tabla de variables solo por su encabezado.
#include <stdio.h>

#include "env.h"

int main(void) {
    Env *env = env_new(NULL);
    if (env == NULL) {
        fprintf(stderr, "no hay memoria\n");
        return 1;
    }
    if (!env_set(env, "x", 4.0) || !env_set(env, "y", 2.5) || !env_set(env, "x", 10.0)) {
        fprintf(stderr, "no hay memoria\n");
        env_free(env);
        return 1;
    }

    double value = -1.0;
    if (env_get(env, "x", &value)) {
        printf("x vale %g\n", value);
    }
    if (env_get(env, "y", &value)) {
        printf("y vale %g\n", value);
    }
    if (!env_get(env, "z", &value)) {
        printf("z no existe, y value sigue en %g\n", value);
    }

    env_free(env);
    return 0;
}
logic_demo.cDescargar logic_demo.c
// Construye una fórmula, la recorre y escribe su tabla de verdad.
#include <stdio.h>

#include "logic.h"

int main(void) {
    // (a and not b) or c
    Logic *formula = logic_or(logic_and(logic_input('a'), logic_not(logic_input('b'))),
                              logic_input('c'));
    if (formula == NULL) {
        fprintf(stderr, "no hay memoria\n");
        return 1;
    }

    logic_print(formula, stdout);
    printf("\nnodos: %d\n", logic_count(formula));
    printf("profundidad: %d\n", logic_depth(formula));

    printf("a b c | resultado\n");
    int inputs[26] = {0};
    for (int a = 0; a <= 1; a++) {
        for (int b = 0; b <= 1; b++) {
            for (int c = 0; c <= 1; c++) {
                inputs['a' - 'a'] = a;
                inputs['b' - 'a'] = b;
                inputs['c' - 'a'] = c;
                printf("%d %d %d | %d\n", a, b, c, logic_eval(formula, inputs));
            }
        }
    }

    logic_free(formula);
    return 0;
}
logic_free_order.cDescargar logic_free_order.c
// Libera un árbol escribiendo qué nodo libera en cada momento.
#include <stdio.h>
#include <stdlib.h>

#include "logic.h"

static void free_traced(Logic *e) {
    if (e == NULL) {
        return;
    }
    switch (e->kind) {
    case LOGIC_CONSTANT:
    case LOGIC_INPUT:
        break;
    case LOGIC_NOT:
        free_traced(e->as.child);
        break;
    case LOGIC_AND:
    case LOGIC_OR:
        free_traced(e->as.pair.left);
        free_traced(e->as.pair.right);
        break;
    }
    printf("libero ");
    if (e->kind == LOGIC_NOT) {
        printf("un not\n");
    } else if (e->kind == LOGIC_AND) {
        printf("un and\n");
    } else if (e->kind == LOGIC_OR) {
        printf("un or\n");
    } else {
        logic_print(e, stdout);
        printf("\n");
    }
    free(e);
}

int main(void) {
    Logic *formula = logic_or(logic_and(logic_input('a'), logic_not(logic_input('b'))),
                              logic_input('c'));
    if (formula == NULL) {
        fprintf(stderr, "no hay memoria\n");
        return 1;
    }
    free_traced(formula);
    return 0;
}
logic_simplify_demo.cDescargar logic_simplify_demo.c
// Simplifica varias fórmulas y cuenta cuántos nodos quedan.
#include <stdio.h>

#include "logic.h"

static int show_simplified(Logic *e) {
    if (e == NULL) {
        fprintf(stderr, "no hay memoria\n");
        return 0;
    }
    int before = logic_count(e);
    logic_print(e, stdout);
    e = logic_simplify(e);
    printf("  ->  ");
    logic_print(e, stdout);
    printf("  (nodos: de %d a %d)\n", before, logic_count(e));
    logic_free(e);
    return 1;
}

int main(void) {
    int ok = show_simplified(logic_and(logic_input('a'), logic_constant(1)));
    ok = show_simplified(logic_and(logic_or(logic_input('a'), logic_input('b')), logic_constant(0))) && ok;
    ok = show_simplified(logic_not(logic_not(logic_input('a')))) && ok;
    ok = show_simplified(logic_or(logic_and(logic_constant(1), logic_input('a')), logic_not(logic_constant(1)))) && ok;
    ok = show_simplified(logic_and(logic_input('a'), logic_or(logic_input('b'), logic_input('c')))) && ok;
    return ok ? 0 : 1;
}
logic.cDescargar logic.c
#include "logic.h"

#include <stdlib.h>

// Reserva un nodo con su etiqueta. El resto lo llena quien llama.
static Logic *logic_node(LogicKind kind) {
    Logic *e = malloc(sizeof *e);
    if (e == NULL) {
        return NULL;
    }
    e->kind = kind;
    return e;
}

Logic *logic_constant(int value) {
    Logic *e = logic_node(LOGIC_CONSTANT);
    if (e == NULL) {
        return NULL;
    }
    e->as.constant = value;
    return e;
}

Logic *logic_input(char letter) {
    Logic *e = logic_node(LOGIC_INPUT);
    if (e == NULL) {
        return NULL;
    }
    e->as.input = letter;
    return e;
}

Logic *logic_not(Logic *child) {
    if (child == NULL) {
        return NULL;
    }
    Logic *e = logic_node(LOGIC_NOT);
    if (e == NULL) {
        logic_free(child);
        return NULL;
    }
    e->as.child = child;
    return e;
}

// AND y OR solo se diferencian en la etiqueta.
static Logic *logic_pair(LogicKind kind, Logic *left, Logic *right) {
    if (left == NULL || right == NULL) {
        logic_free(left);
        logic_free(right);
        return NULL;
    }
    Logic *e = logic_node(kind);
    if (e == NULL) {
        logic_free(left);
        logic_free(right);
        return NULL;
    }
    e->as.pair.left = left;
    e->as.pair.right = right;
    return e;
}

Logic *logic_and(Logic *left, Logic *right) {
    return logic_pair(LOGIC_AND, left, right);
}

Logic *logic_or(Logic *left, Logic *right) {
    return logic_pair(LOGIC_OR, left, right);
}

void logic_free(Logic *e) {
    if (e == NULL) {
        return;
    }
    switch (e->kind) {
    case LOGIC_CONSTANT:
    case LOGIC_INPUT:
        break;
    case LOGIC_NOT:
        logic_free(e->as.child);
        break;
    case LOGIC_AND:
    case LOGIC_OR:
        logic_free(e->as.pair.left);
        logic_free(e->as.pair.right);
        break;
    }
    free(e);
}

int logic_count(const Logic *e) {
    switch (e->kind) {
    case LOGIC_CONSTANT:
    case LOGIC_INPUT:
        return 1;
    case LOGIC_NOT:
        return 1 + logic_count(e->as.child);
    case LOGIC_AND:
    case LOGIC_OR:
        return 1 + logic_count(e->as.pair.left) + logic_count(e->as.pair.right);
    }
    return 0;
}

int logic_depth(const Logic *e) {
    switch (e->kind) {
    case LOGIC_CONSTANT:
    case LOGIC_INPUT:
        return 1;
    case LOGIC_NOT:
        return 1 + logic_depth(e->as.child);
    case LOGIC_AND:
    case LOGIC_OR: {
        int left = logic_depth(e->as.pair.left);
        int right = logic_depth(e->as.pair.right);
        return 1 + (left > right ? left : right);
    }
    }
    return 0;
}

// Escribe la fórmula como llamadas: and(a, not(b)). Así no hacen falta reglas de paréntesis.
void logic_print(const Logic *e, FILE *out) {
    switch (e->kind) {
    case LOGIC_CONSTANT:
        fputs(e->as.constant ? "true" : "false", out);
        break;
    case LOGIC_INPUT:
        fputc(e->as.input, out);
        break;
    case LOGIC_NOT:
        fputs("not(", out);
        logic_print(e->as.child, out);
        fputc(')', out);
        break;
    case LOGIC_AND:
    case LOGIC_OR:
        fputs(e->kind == LOGIC_AND ? "and(" : "or(", out);
        logic_print(e->as.pair.left, out);
        fputs(", ", out);
        logic_print(e->as.pair.right, out);
        fputc(')', out);
        break;
    }
}

int logic_eval(const Logic *e, const int *inputs) {
    switch (e->kind) {
    case LOGIC_CONSTANT:
        return e->as.constant;
    case LOGIC_INPUT:
        return inputs[e->as.input - 'a'];
    case LOGIC_NOT:
        return !logic_eval(e->as.child, inputs);
    case LOGIC_AND:
        return logic_eval(e->as.pair.left, inputs) && logic_eval(e->as.pair.right, inputs);
    case LOGIC_OR:
        return logic_eval(e->as.pair.left, inputs) || logic_eval(e->as.pair.right, inputs);
    }
    return 0;
}

static int is_constant(const Logic *e, int value) {
    return e->kind == LOGIC_CONSTANT && e->as.constant == value;
}

// Conserva un hijo, y libera al otro hijo y al nodo. Devuelve el que se conserva.
static Logic *keep_child(Logic *node, Logic *kept, Logic *dropped) {
    logic_free(dropped);
    free(node);
    return kept;
}

static Logic *simplify_not(Logic *e) {
    e->as.child = logic_simplify(e->as.child);
    Logic *child = e->as.child;
    if (child->kind == LOGIC_CONSTANT) {
        child->as.constant = !child->as.constant;
        free(e);
        return child;
    }
    if (child->kind == LOGIC_NOT) {
        Logic *inner = child->as.child;
        free(child);
        free(e);
        return inner;
    }
    return e;
}

static Logic *simplify_pair(Logic *e) {
    e->as.pair.left = logic_simplify(e->as.pair.left);
    e->as.pair.right = logic_simplify(e->as.pair.right);
    Logic *left = e->as.pair.left;
    Logic *right = e->as.pair.right;
    // En AND, true no cambia nada y false decide. En OR, al revés.
    int neutral = e->kind == LOGIC_AND;
    if (is_constant(left, neutral)) {
        return keep_child(e, right, left);
    }
    if (is_constant(right, neutral)) {
        return keep_child(e, left, right);
    }
    if (is_constant(left, !neutral)) {
        return keep_child(e, left, right);
    }
    if (is_constant(right, !neutral)) {
        return keep_child(e, right, left);
    }
    return e;
}

Logic *logic_simplify(Logic *e) {
    if (e == NULL) {
        return NULL;
    }
    switch (e->kind) {
    case LOGIC_CONSTANT:
    case LOGIC_INPUT:
        return e;
    case LOGIC_NOT:
        return simplify_not(e);
    case LOGIC_AND:
    case LOGIC_OR:
        return simplify_pair(e);
    }
    return e;
}
logic.hDescargar logic.h
// Fórmulas lógicas como árboles: el ejemplo de la clase.
//
// Ownership: cada nodo es owner de sus hijos. Quien recibe un árbol lo libera con logic_free.
// Un constructor que recibe hijos se queda con ellos, también cuando falla.
#ifndef LOGIC_H
#define LOGIC_H

#include <stdio.h>

typedef enum { LOGIC_CONSTANT, LOGIC_INPUT, LOGIC_NOT, LOGIC_AND, LOGIC_OR } LogicKind;

typedef struct logic Logic;

struct logic {
    LogicKind kind;
    union {
        int constant;                                // LOGIC_CONSTANT: 0 o 1
        char input;                                  // LOGIC_INPUT: una letra de 'a' a 'z'
        Logic *child;                                // LOGIC_NOT
        struct { Logic *left; Logic *right; } pair;  // LOGIC_AND y LOGIC_OR
    } as;
};

// Devuelven NULL si no hay memoria. Si un hijo es NULL, liberan al otro y devuelven NULL.
Logic *logic_constant(int value);
Logic *logic_input(char letter);
Logic *logic_not(Logic *child);
Logic *logic_and(Logic *left, Logic *right);
Logic *logic_or(Logic *left, Logic *right);

// Libera el árbol completo. Acepta NULL.
void logic_free(Logic *e);

// Consultas: no cambian el árbol.
int logic_count(const Logic *e);
int logic_depth(const Logic *e);
void logic_print(const Logic *e, FILE *out);

// inputs tiene 26 valores, uno por letra: inputs[0] es 'a'.
int logic_eval(const Logic *e, const int *inputs);

// Se queda con e y devuelve el árbol simplificado. Lo que ya no se usa, lo libera.
Logic *logic_simplify(Logic *e);

#endif
node_sizes.cDescargar node_sizes.c
// Todos los nodos de un tipo miden lo mismo, sea cual sea el tamaño del árbol.
#include <stdio.h>

#include "expr.h"
#include "logic.h"

int main(void) {
    printf("sizeof(Logic) = %zu\n", sizeof(Logic));
    printf("sizeof(Expr) = %zu\n", sizeof(Expr));
    return 0;
}

Errores

Ejemplos con errores a propósito: no compilan, fallan o dan un resultado incorrecto. Los apuntes explican cada uno.

errors/free_only_root.cDescargar errors/free_only_root.c
// Error a propósito: libera la raíz y pierde los hijos.
#include <stdlib.h>

#include "logic.h"

int main(void) {
    Logic *formula = logic_and(logic_input('a'), logic_not(logic_input('b')));
    free(formula);
    return 0;
}
errors/free_parent_first.cDescargar errors/free_parent_first.c
// Error a propósito: libera el nodo y después lee sus hijos.
#include <stdlib.h>

#include "logic.h"

static void bad_free(Logic *e) {
    if (e == NULL) {
        return;
    }
    free(e);
    if (e->kind == LOGIC_AND) {
        bad_free(e->as.pair.left);
        bad_free(e->as.pair.right);
    }
}

int main(void) {
    bad_free(logic_and(logic_input('a'), logic_input('b')));
    return 0;
}
errors/missing_case.cDescargar errors/missing_case.c
// Error a propósito: falta un caso en el switch. -Wall lo avisa.
#include <stdio.h>

#include "logic.h"

static int count_inputs(const Logic *e) {
    switch (e->kind) {
    case LOGIC_CONSTANT:
        return 0;
    case LOGIC_INPUT:
        return 1;
    case LOGIC_NOT:
        return count_inputs(e->as.child);
    case LOGIC_AND:
        return count_inputs(e->as.pair.left) + count_inputs(e->as.pair.right);
    }
    return 0;
}

int main(void) {
    Logic *formula = logic_or(logic_input('a'), logic_input('b'));
    if (formula == NULL) {
        return 1;
    }
    printf("entradas: %d\n", count_inputs(formula));
    logic_free(formula);
    return 0;
}
errors/self_by_value.cDescargar errors/self_by_value.c
// Error a propósito: un nodo no puede contener otro nodo entero.
struct node {
    int kind;
    struct node left;
    struct node right;
};

int main(void) {
    return 0;
}
errors/shared_child.cDescargar errors/shared_child.c
// Error a propósito: el mismo nodo es hijo dos veces, y por eso tiene dos owners.
#include <stdio.h>

#include "logic.h"

int main(void) {
    Logic *a = logic_input('a');
    Logic *formula = logic_and(a, a);
    if (formula == NULL) {
        return 1;
    }
    logic_print(formula, stdout);
    printf("\n");
    logic_free(formula);
    return 0;
}
errors/simplify_dangling.cDescargar errors/simplify_dangling.c
// Error a propósito: libera el árbol entero, y con él el hijo que iba a conservar.
#include <stdio.h>

#include "logic.h"

static Logic *bad_simplify(Logic *e) {
    if (e->kind == LOGIC_AND && e->as.pair.right->kind == LOGIC_CONSTANT
        && e->as.pair.right->as.constant == 1) {
        Logic *left = e->as.pair.left;
        logic_free(e);
        return left;
    }
    return e;
}

int main(void) {
    Logic *formula = logic_and(logic_input('a'), logic_constant(1));
    formula = bad_simplify(formula);
    logic_print(formula, stdout);
    printf("\n");
    logic_free(formula);
    return 0;
}
errors/simplify_leak.cDescargar errors/simplify_leak.c
// Error a propósito: al simplificar "a and false" descarta el subárbol sin liberarlo.
#include <stdio.h>
#include <stdlib.h>

#include "logic.h"

static Logic *bad_simplify(Logic *e) {
    if (e->kind == LOGIC_AND && e->as.pair.right->kind == LOGIC_CONSTANT
        && e->as.pair.right->as.constant == 0) {
        Logic *right = e->as.pair.right;
        free(e);
        return right;
    }
    return e;
}

int main(void) {
    Logic *formula = logic_and(logic_or(logic_input('a'), logic_input('b')), logic_constant(0));
    formula = bad_simplify(formula);
    logic_print(formula, stdout);
    printf("\n");
    logic_free(formula);
    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"
BASE = ["-Wall", "-Wextra", "-std=c17"]
FLAGS = [*BASE, "-g", "-fsanitize=address,undefined"]
PROGRAMS = ["logic_demo", "logic_free_order", "logic_simplify_demo", "node_sizes", "env_demo"]
SOURCES = ["logic.c", "logic_demo.c", "logic_free_order.c", "logic_simplify_demo.c", "node_sizes.c", "env_demo.c"]


def run(program):
    done = subprocess.run(
        [str(program)], cwd=HERE, text=True, encoding="utf-8", errors="replace",
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=30,
    )
    return done.stdout, done.returncode


def build(sources, flags=FLAGS):
    """Compila y devuelve (ruta del ejecutable, carpeta temporal, mensajes de gcc, código)."""
    folder = tempfile.TemporaryDirectory()
    binary = pathlib.Path(folder.name) / "program"
    done = subprocess.run(
        ["gcc", *flags, "-I.", "-Ilab", *sources, "-o", str(binary)], cwd=HERE, text=True,
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
    )
    return binary, folder, done.stdout, done.returncode


def report(source):
    binary, folder, _, code = build([source, "logic.c"])
    assert code == 0
    out, code = run(binary)
    folder.cleanup()
    return out, code


class Programs(unittest.TestCase):
    def test_every_source_compiles_without_warnings(self):
        for name in SOURCES:
            with self.subTest(source=name):
                subprocess.run(
                    ["gcc", *FLAGS, "-Werror", "-I.", "-Ilab", "-c", name, "-o", "/dev/null"],
                    cwd=HERE, check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                )

    def test_every_program_ends_without_reports(self):
        for name in PROGRAMS:
            with self.subTest(program=name):
                out, code = run(BUILD / name)
                self.assertEqual(code, 0, out)
                self.assertNotIn("runtime error", out)
                self.assertNotIn("AddressSanitizer", out)

    def test_the_tree_of_the_notes(self):
        lines = run(BUILD / "logic_demo")[0].splitlines()
        self.assertEqual(lines[0], "or(and(a, not(b)), c)")
        self.assertEqual(lines[1], "nodos: 6")
        self.assertEqual(lines[2], "profundidad: 4")
        # La tabla de verdad coincide con (a and not b) or c, escrita a mano.
        for line in lines[4:]:
            a, b, c, _, result = line.split()
            wanted = (a == "1" and b == "0") or c == "1"
            self.assertEqual(result == "1", wanted, line)

    def test_children_are_freed_before_their_parent(self):
        lines = run(BUILD / "logic_free_order")[0].splitlines()
        self.assertLess(lines.index("libero b"), lines.index("libero un not"))
        self.assertLess(lines.index("libero un not"), lines.index("libero un and"))
        self.assertEqual(lines[-1], "libero un or")
        self.assertEqual(len(lines), 6)

    def test_simplifying_does_not_change_the_kept_formula(self):
        lines = run(BUILD / "logic_simplify_demo")[0].splitlines()
        self.assertEqual(lines[-1], "and(a, or(b, c))  ->  and(a, or(b, c))  (nodos: de 5 a 5)")

    def test_env_is_used_only_through_its_header(self):
        self.assertNotIn("struct binding", (HERE / "env_demo.c").read_text(encoding="utf-8"))
        self.assertNotIn("env.c", (HERE / "env_demo.c").read_text(encoding="utf-8"))


class CompilerClaims(unittest.TestCase):
    def test_a_node_cannot_contain_itself(self):
        _, folder, text, code = build(["errors/self_by_value.c"], BASE)
        folder.cleanup()
        self.assertNotEqual(code, 0)
        self.assertIn("has incomplete type", text)

    def test_a_missing_case_gives_a_warning(self):
        _, folder, text, code = build(["errors/missing_case.c", "logic.c"], BASE)
        folder.cleanup()
        self.assertEqual(code, 0)
        self.assertIn("LOGIC_OR", text)
        self.assertIn("-Wswitch", text)

    def test_a_switch_needs_the_final_return(self):
        with tempfile.TemporaryDirectory() as name:
            path = pathlib.Path(name) / "no_return.c"
            text = (HERE / "logic.c").read_text(encoding="utf-8")
            cut = text.index("int logic_count(")
            end = text.index("int logic_depth(")
            path.write_text('#include "logic.h"\n' + text[cut:end].replace("    return 0;\n", ""), encoding="utf-8")
            done = subprocess.run(["gcc", *BASE, "-I.", "-c", str(path), "-o", "/dev/null"], cwd=HERE, text=True,
                                  stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        self.assertIn("-Wreturn-type", done.stdout)

    def test_strdup_is_not_declared_in_c17(self):
        with tempfile.TemporaryDirectory() as name:
            path = pathlib.Path(name) / "dup.c"
            path.write_text('#include <string.h>\nchar *copy(const char *s) { return strdup(s); }\n', encoding="utf-8")
            done = subprocess.run(["gcc", *BASE, "-c", str(path), "-o", "/dev/null"], text=True,
                                  stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        self.assertIn("strdup", done.stdout)

    def test_negative_zero_is_written_with_its_sign(self):
        with tempfile.TemporaryDirectory() as name:
            path = pathlib.Path(name) / "zero.c"
            binary = pathlib.Path(name) / "zero"
            path.write_text('#include <stdio.h>\nint main(void) { volatile double a = -3.0; '
                            'printf("%g\\n", a * 0.0); return 0; }\n', encoding="utf-8")
            subprocess.run(["gcc", *BASE, str(path), "-o", str(binary)], check=True)
            self.assertEqual(run(binary)[0].strip(), "-0")

    def test_gcc_warns_about_freeing_the_parent_first(self):
        _, folder, text, code = build(["errors/free_parent_first.c", "logic.c"])
        folder.cleanup()
        self.assertEqual(code, 0)
        self.assertIn("-Wuse-after-free", text)

    def test_the_other_errors_give_no_warning(self):
        for source in ("errors/free_only_root.c", "errors/shared_child.c", "errors/simplify_leak.c",
                       "errors/simplify_dangling.c"):
            with self.subTest(source=source):
                _, folder, text, code = build([source, "logic.c"])
                folder.cleanup()
                self.assertEqual(code, 0)
                self.assertEqual(text, "")


class RunClaims(unittest.TestCase):
    def test_freeing_the_parent_first_is_use_after_free(self):
        out, code = report("errors/free_parent_first.c")
        self.assertNotEqual(code, 0)
        self.assertIn("heap-use-after-free", out)

    def test_freeing_only_the_root_leaks(self):
        out, code = report("errors/free_only_root.c")
        self.assertNotEqual(code, 0)
        self.assertIn("detected memory leaks", out)

    def test_a_shared_child_is_freed_twice(self):
        out, code = report("errors/shared_child.c")
        self.assertNotEqual(code, 0)
        self.assertIn("heap-use-after-free", out)

    def test_dropping_a_subtree_leaks(self):
        out, code = report("errors/simplify_leak.c")
        self.assertNotEqual(code, 0)
        self.assertIn("in 3 allocation(s)", out)

    def test_freeing_the_kept_child_is_use_after_free(self):
        out, code = report("errors/simplify_dangling.c")
        self.assertNotEqual(code, 0)
        self.assertIn("heap-use-after-free", out)


class LabIsReady(unittest.TestCase):
    def test_env_files_are_there(self):
        for name in ("env.h", "env.c", "expr.h", "expr.c", "expr_tests.c", "algebra.c", "README.md"):
            self.assertTrue((HERE / "lab" / name).exists(), name)

    def test_lab_compiles_without_warnings(self):
        for name in ("env.c", "expr.c", "expr_tests.c", "algebra.c"):
            with self.subTest(source=name):
                subprocess.run(
                    ["gcc", *FLAGS, "-Werror", "-Ilab", "-c", f"lab/{name}", "-o", "/dev/null"],
                    cwd=HERE, check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                )


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 prueba compila un main propio con su expr.c y corre con ASan y UBSan: una fuga, un uso de
memoria liberada o un aviso también hacen fallar la prueba. Las pruebas de la sesión 2 miran los
árboles sin usar expr_print ni expr_eval, para que un error en esas no se confunda con otro.
"""
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"]
WRAP = ["-Wl,--wrap=malloc,--wrap=calloc,--wrap=realloc"]

PRELUDE = r"""
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "expr.h"

#define N expr_number
#define V expr_variable
#define ADD(a, b) expr_binary(OP_ADD, (a), (b))
#define SUB(a, b) expr_binary(OP_SUB, (a), (b))
#define MUL(a, b) expr_binary(OP_MUL, (a), (b))
#define DIV(a, b) expr_binary(OP_DIV, (a), (b))

// Falla la prueba de inmediato, con un mensaje claro.
static void fail(const char *message) {
    printf("%s\n", message);
    fflush(stdout);
    _Exit(3);
}

static Expr *need(Expr *e) {
    if (e == NULL) {
        fail("un constructor devolvió NULL");
    }
    return e;
}

// Escribe el árbol en prefijo, sin usar expr_print: (+ 1 x)
static void dump(const Expr *e) {
    if (e == NULL) {
        printf("NULL");
        return;
    }
    switch (e->kind) {
    case EXPR_NUMBER:
        printf("%g", e->as.number);
        break;
    case EXPR_VARIABLE:
        printf("%s", e->as.name);
        break;
    case EXPR_BINARY:
        printf("(%c ", "+-*/"[e->as.binary.op]);
        dump(e->as.binary.left);
        printf(" ");
        dump(e->as.binary.right);
        printf(")");
        break;
    }
}

// Escribe lo que expr_print deja en un archivo, y libera el árbol.
static void show_print(Expr *e) {
    need(e);
    FILE *file = tmpfile();
    if (file == NULL) {
        fail("no se pudo crear un archivo temporal");
    }
    expr_print(e, file);
    rewind(file);
    int c;
    printf("[");
    while ((c = fgetc(file)) != EOF) {
        putchar(c);
    }
    printf("]\n");
    fclose(file);
    expr_free(e);
}

// Simplifica, escribe en prefijo y libera.
static void show_simplified(Expr *e) {
    need(e);
    Expr *result = expr_simplify(e);
    dump(result);
    printf("\n");
    if (result != NULL) {
        expr_free(result);
    }
}

// Evalúa sin usar expr_eval. Las únicas variables son x e y.
static double own_eval(const Expr *e, double x, double y) {
    switch (e->kind) {
    case EXPR_NUMBER:
        return e->as.number;
    case EXPR_VARIABLE:
        if (strcmp(e->as.name, "x") == 0) {
            return x;
        }
        if (strcmp(e->as.name, "y") == 0) {
            return y;
        }
        fail("apareció una variable que no estaba en la expresión");
        return 0.0;
    case EXPR_BINARY: {
        double l = own_eval(e->as.binary.left, x, y);
        double r = own_eval(e->as.binary.right, x, y);
        switch (e->as.binary.op) {
        case OP_ADD: return l + r;
        case OP_SUB: return l - r;
        case OP_MUL: return l * r;
        case OP_DIV: return l / r;
        }
    }
    }
    return 0.0;
}

// Compara la derivada respecto de x con la pendiente numérica, en un punto.
static void check_derivative(Expr *e, double x, double y) {
    need(e);
    Expr *d = expr_derive(e, "x");
    if (d == NULL) {
        fail("expr_derive devolvió NULL");
    }
    double h = 0.00001;
    double approx = (own_eval(e, x + h, y) - own_eval(e, x - h, y)) / (2 * h);
    double exact = own_eval(d, x, y);
    if (fabs(approx - exact) > 0.0001) {
        printf("derivada de ");
        dump(e);
        printf(" en x = %g: su árbol da %g, y debería dar %g. Su árbol es ", x, exact, approx);
        dump(d);
        printf("\n");
        fflush(stdout);
        _Exit(3);
    }
    expr_free(d);
    expr_free(e);
}
"""

# Hace fallar la k-ésima reserva de memoria, para probar los caminos de error.
FAILING_MALLOC = r"""
static int countdown = -1;
static int failed = 0;
void *__real_malloc(size_t size);
void *__real_calloc(size_t count, size_t size);
void *__real_realloc(void *block, size_t size);
static int must_fail(void) {
    if (countdown == 0) {
        countdown = -1;
        failed = 1;
        return 1;
    }
    if (countdown > 0) {
        countdown--;
    }
    return 0;
}
void *__wrap_malloc(size_t size) { return must_fail() ? NULL : __real_malloc(size); }
void *__wrap_calloc(size_t count, size_t size) { return must_fail() ? NULL : __real_calloc(count, size); }
void *__wrap_realloc(void *block, size_t size) { return must_fail() ? NULL : __real_realloc(block, size); }

// Repite la operación haciendo fallar la reserva número 0, 1, 2... hasta que ninguna falle.
#define EACH_FAILURE(result, operation)                                          \
    for (int k = 0; ; k++) {                                                     \
        countdown = k;                                                           \
        failed = 0;                                                              \
        Expr *result = (operation);                                              \
        countdown = -1;                                                          \
        if (failed && result != NULL) {                                          \
            fail("no había memoria, y aun así no devolvió NULL");                \
        }                                                                        \
        if (!failed && result == NULL) {                                         \
            fail("había memoria, y aun así devolvió NULL");                      \
        }                                                                        \
        int done = !failed;                                                      \
        expr_free(result);                                                       \
        if (done) {                                                              \
            printf("reservas: %d\n", k);                                         \
            break;                                                               \
        }                                                                        \
    }
"""


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 explain(out, code):
    if "detected memory leaks" in out:
        head = "Hay una fuga de memoria: algo se reservó y no se liberó."
    elif "heap-use-after-free" in out or "double-free" in out:
        head = "Se usó o se liberó memoria que ya estaba liberada."
    elif "SEGV" in out:
        head = "El programa falló por acceder a una dirección inválida, quizás un NULL."
    else:
        head = f"El programa terminó con error (código {code})."
    return head + "\n" + "\n".join(out.splitlines()[:25])


def expr_sources():
    return [str(source("expr.c")), str(LAB / "env.c")]


def build_and_run(body, wrap=False):
    """Devuelve (salida, código de salida) de un main propio, en una carpeta temporal."""
    with tempfile.TemporaryDirectory() as name:
        folder = pathlib.Path(name)
        binary = folder / "driver"
        driver = folder / "driver.c"
        extra = FAILING_MALLOC if wrap else ""
        driver.write_text(f"{PRELUDE}\n{extra}\nint main(void) {{\n{body}\nreturn 0;\n}}\n", encoding="utf-8")
        built = subprocess.run(
            ["gcc", "-std=c17", "-w", "-g", "-fsanitize=address,undefined", f"-I{LAB}", str(driver),
             *expr_sources(), "-lm", *(WRAP if wrap else []), "-o", str(binary)],
            text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
        )
        if built.returncode != 0:
            raise AssertionError(f"No compila:\n{built.stdout}")
        try:
            done = subprocess.run(
                [str(binary)], cwd=folder, text=True, encoding="utf-8", errors="replace",
                stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=60,
            )
        except subprocess.TimeoutExpired:
            raise AssertionError("El programa no terminó en 60 segundos.") from None
        return done.stdout, done.returncode


@unittest.skipUnless((SOURCE / "expr.c").exists(), f"no está {LAB_DIR}/expr.c")
class LabCase(unittest.TestCase):
    longMessage = False

    def lines(self, body, wanted, what, wrap=False):
        """Corre body y compara su salida, línea por línea, con la lista wanted."""
        out, code = build_and_run(body, wrap)
        if code != 0:
            self.fail(f"{what}: {explain(out, code)}")
        got = out.splitlines()
        for i, line in enumerate(wanted):
            if i >= len(got):
                self.fail(f"{what}: faltan líneas. Se esperaba: {line}")
            if got[i] != line:
                self.fail(f"{what}, caso {i + 1}\n  se esperaba: {line}\n  se obtuvo:   {got[i]}")
        if len(got) > len(wanted):
            self.fail(f"{what}: sobran líneas. Lo que sobra: {got[len(wanted)]}")

    def ok(self, body, what, wrap=False):
        out, code = build_and_run(body, wrap)
        if code != 0:
            self.fail(f"{what}: {explain(out, code)}")
        return out


# ---------------------------------------------------------------- Sesión 1


class Constructors(LabCase):
    def test_number(self):
        self.ok(r"""
        Expr *e = need(N(2.5));
        if (e->kind != EXPR_NUMBER) fail("la etiqueta no es EXPR_NUMBER");
        if (e->as.number != 2.5) fail("el número no se guardó");
        free(e);
        """, "expr_number")

    def test_variable_keeps_its_own_copy(self):
        self.ok(r"""
        char name[] = "total";
        Expr *e = need(V(name));
        if (e->kind != EXPR_VARIABLE) fail("la etiqueta no es EXPR_VARIABLE");
        if (e->as.name == name) fail("guardó la dirección que recibió, y no una copia");
        name[0] = 'X';
        if (strcmp(e->as.name, "total") != 0) fail("el nombre cambió al cambiar el original");
        expr_free(e);
        """, "expr_variable")

    def test_binary_links_its_children(self):
        self.ok(r"""
        Expr *left = need(N(1));
        Expr *right = need(V("x"));
        Expr *e = need(SUB(left, right));
        if (e->kind != EXPR_BINARY) fail("la etiqueta no es EXPR_BINARY");
        if (e->as.binary.op != OP_SUB) fail("el operador no se guardó");
        if (e->as.binary.left != left) fail("el hijo izquierdo no es el que recibió");
        if (e->as.binary.right != right) fail("el hijo derecho no es el que recibió");
        expr_free(e);
        """, "expr_binary")

    def test_binary_owns_its_children_when_one_is_null(self):
        self.ok(r"""
        Expr *e = need(ADD(N(1), N(2)));
        expr_free(e);
        if (expr_binary(OP_ADD, NULL, need(N(1))) != NULL) fail("con el izquierdo NULL debe devolver NULL");
        if (expr_binary(OP_ADD, need(V("x")), NULL) != NULL) fail("con el derecho NULL debe devolver NULL");
        if (expr_binary(OP_ADD, NULL, NULL) != NULL) fail("con los dos NULL debe devolver NULL");
        """, "expr_binary con un hijo NULL")

    def test_no_memory(self):
        self.ok(r"""
        EACH_FAILURE(a, V("nombre"))
        EACH_FAILURE(b, N(1))
        EACH_FAILURE(c, ADD(V("x"), MUL(N(2), V("y"))))
        """, "sin memoria", wrap=True)


class Free(LabCase):
    def test_frees_the_whole_tree(self):
        self.ok(r"""
        expr_free(NULL);
        Expr *e = need(ADD(MUL(N(2), V("largo")), DIV(SUB(V("x"), N(1)), V("y"))));
        expr_free(e);
        """, "expr_free")


class Print(LabCase):
    def test_leaves(self):
        self.lines(r"""
        show_print(N(2));
        show_print(N(2.5));
        show_print(N(-3));
        show_print(V("total"));
        """, ["[2]", "[2.5]", "[-3]", "[total]"], "expr_print de una hoja")

    def test_writes_where_it_is_told(self):
        out = self.ok(r"""
        Expr *e = need(ADD(N(1), V("x")));
        FILE *file = tmpfile();
        if (file == NULL) fail("no se pudo crear un archivo temporal");
        expr_print(e, file);
        if (ftell(file) == 0) fail("no escribió nada en out");
        fclose(file);
        expr_free(e);
        """, "expr_print")
        self.assertEqual(out, "", "expr_print escribió en la pantalla, y debía escribir en out.")

    def test_precedence(self):
        self.lines(r"""
        show_print(ADD(N(2), MUL(N(3), V("x"))));
        show_print(MUL(ADD(N(2), N(3)), V("x")));
        show_print(MUL(V("x"), ADD(N(2), N(3))));
        show_print(ADD(MUL(V("x"), V("y")), V("z")));
        show_print(DIV(SUB(V("a"), V("b")), ADD(V("c"), V("d"))));
        show_print(SUB(MUL(V("a"), V("b")), DIV(V("c"), V("d"))));
        """, ["[2 + 3 * x]", "[(2 + 3) * x]", "[x * (2 + 3)]", "[x * y + z]", "[(a - b) / (c + d)]",
              "[a * b - c / d]"], "expr_print: precedencia")

    def test_same_precedence(self):
        self.lines(r"""
        show_print(SUB(SUB(N(1), N(2)), N(3)));
        show_print(SUB(N(1), SUB(N(2), N(3))));
        show_print(DIV(DIV(N(8), N(2)), N(2)));
        show_print(DIV(N(8), DIV(N(2), N(2))));
        show_print(DIV(N(8), MUL(N(2), N(2))));
        show_print(MUL(N(8), DIV(N(2), N(2))));
        show_print(SUB(N(1), ADD(N(2), N(3))));
        show_print(ADD(N(1), ADD(N(2), N(3))));
        show_print(ADD(ADD(N(1), N(2)), N(3)));
        """, ["[1 - 2 - 3]", "[1 - (2 - 3)]", "[8 / 2 / 2]", "[8 / (2 / 2)]", "[8 / (2 * 2)]",
              "[8 * (2 / 2)]", "[1 - (2 + 3)]", "[1 + (2 + 3)]", "[1 + 2 + 3]"],
                   "expr_print: la misma precedencia")


EVAL_MAIN = r"""
Env *env = env_new(NULL);
if (env == NULL || !env_set(env, "x", 4.0) || !env_set(env, "y", 0.0)) fail("no se pudo crear el entorno");
double value = -7.0;
"""


def eval_line(tree):
    return (f"{{ e = need({tree}); value = -7.0; "
            f'int ok = expr_eval(e, env, &value); printf("%d %g\\n", ok, value); expr_free(e); }}')


class Eval(LabCase):
    def body(self, *trees):
        return EVAL_MAIN + "Expr *e;\n" + "\n".join(eval_line(tree) for tree in trees) + "\nenv_free(env);"

    def test_leaves(self):
        self.lines(self.body('N(2.5)', 'V("x")'), ["1 2.5", "1 4"], "expr_eval de una hoja")

    def test_operations_in_order(self):
        self.lines(self.body('ADD(N(2), MUL(N(3), V("x")))', 'MUL(ADD(N(2), N(3)), V("x"))',
                             'SUB(N(1), SUB(N(2), N(3)))', 'DIV(DIV(N(8), N(2)), V("x"))'),
                   ["1 14", "1 20", "1 2", "1 1"], "expr_eval: valor")

    def test_unknown_variable(self):
        self.lines(self.body('ADD(N(1), N(2))', 'V("z")', 'ADD(V("z"), N(1))', 'ADD(N(1), V("z"))'),
                   ["1 3", "0 -7", "0 -7", "0 -7"], "expr_eval: variable que no existe, sin tocar *result")

    def test_division_by_zero(self):
        self.lines(self.body('DIV(N(1), N(2))', 'DIV(N(1), N(0))', 'DIV(V("x"), V("y"))',
                             'ADD(N(1), DIV(N(1), SUB(V("x"), V("x"))))', 'MUL(DIV(N(1), V("y")), N(0))'),
                   ["1 0.5", "0 -7", "0 -7", "0 -7", "0 -7"], "expr_eval: división entre 0, sin tocar *result")


# ---------------------------------------------------------------- Sesión 2


class Copy(LabCase):
    def test_copy_is_independent(self):
        self.lines(r"""
        Expr *e = need(ADD(MUL(N(2), V("x")), V("y")));
        Expr *copy = expr_copy(e);
        if (copy == NULL) fail("expr_copy devolvió NULL");
        if (copy == e || copy->as.binary.left == e->as.binary.left) fail("la copia comparte nodos");
        if (copy->as.binary.right->as.name == e->as.binary.right->as.name) fail("la copia comparte el nombre");
        expr_free(e);
        dump(copy);
        printf("\n");
        expr_free(copy);
        """, ["(+ (* 2 x) y)"], "expr_copy")

    def test_no_memory(self):
        self.ok(r"""
        Expr *e = need(ADD(MUL(N(2), V("x")), V("y")));
        EACH_FAILURE(copy, expr_copy(e))
        expr_free(e);
        """, "expr_copy sin memoria", wrap=True)


class Equal(LabCase):
    def test_equal_and_different(self):
        self.lines(r"""
        #define SHOW(a, b) { Expr *p = need(a); Expr *q = need(b); printf("%d\n", expr_equal(p, q)); expr_free(p); expr_free(q); }
        SHOW(N(2), N(2))
        SHOW(V("x"), V("x"))
        SHOW(ADD(N(1), MUL(V("x"), N(2))), ADD(N(1), MUL(V("x"), N(2))))
        SHOW(N(2), N(3))
        SHOW(V("x"), V("y"))
        SHOW(N(2), V("x"))
        SHOW(ADD(N(1), N(2)), SUB(N(1), N(2)))
        SHOW(ADD(N(1), N(2)), ADD(N(1), N(3)))
        SHOW(ADD(N(1), N(2)), ADD(N(3), N(2)))
        SHOW(ADD(N(1), N(2)), ADD(N(2), N(1)))
        SHOW(ADD(ADD(N(1), N(2)), N(3)), ADD(N(1), ADD(N(2), N(3))))
        SHOW(ADD(N(1), N(2)), N(3))
        """, ["1", "1", "1"] + ["0"] * 9, "expr_equal")


class Simplify(LabCase):
    def test_neutral_elements(self):
        self.lines(r"""
        show_simplified(ADD(V("x"), N(0)));
        show_simplified(ADD(N(0), V("x")));
        show_simplified(SUB(V("x"), N(0)));
        show_simplified(MUL(V("x"), N(1)));
        show_simplified(MUL(N(1), V("x")));
        show_simplified(DIV(V("x"), N(1)));
        """, ["x"] * 6, "expr_simplify: x + 0, x - 0, x * 1, x / 1")

    def test_times_zero_frees_the_subtree(self):
        self.lines(r"""
        show_simplified(MUL(V("x"), N(0)));
        show_simplified(MUL(N(0), V("x")));
        show_simplified(MUL(ADD(V("x"), MUL(V("y"), SUB(V("z"), N(2)))), N(0)));
        show_simplified(MUL(N(-3), N(0)));
        show_simplified(MUL(DIV(N(1), N(0)), N(0)));
        """, ["0", "0", "0", "0", "0"], "expr_simplify: x * 0")

    def test_constants(self):
        self.lines(r"""
        show_simplified(ADD(N(2), N(3)));
        show_simplified(SUB(N(2), N(3)));
        show_simplified(MUL(N(2), N(3)));
        show_simplified(DIV(N(6), N(4)));
        show_simplified(MUL(ADD(N(1), N(2)), ADD(N(3), N(4))));
        show_simplified(ADD(V("x"), MUL(N(2), N(3))));
        """, ["5", "-1", "6", "1.5", "21", "(+ x 6)"], "expr_simplify: constantes")

    def test_children_first(self):
        self.lines(r"""
        show_simplified(ADD(MUL(V("x"), N(0)), N(3)));
        show_simplified(ADD(MUL(V("x"), N(0)), MUL(V("y"), N(1))));
        show_simplified(MUL(ADD(V("x"), SUB(N(1), N(1))), SUB(N(3), N(2))));
        show_simplified(DIV(ADD(N(0), V("x")), ADD(N(1), N(0))));
        """, ["3", "y", "x", "x"], "expr_simplify: primero los hijos")

    def test_leaves_alone_what_it_should(self):
        self.lines(r"""
        show_simplified(ADD(V("x"), N(0)));
        show_simplified(DIV(N(1), N(0)));
        show_simplified(SUB(N(0), V("x")));
        show_simplified(DIV(N(0), V("x")));
        show_simplified(DIV(N(1), V("x")));
        show_simplified(ADD(V("x"), V("y")));
        show_simplified(N(4));
        show_simplified(V("x"));
        if (expr_simplify(NULL) != NULL) fail("con NULL debe devolver NULL");
        """, ["x", "(/ 1 0)", "(- 0 x)", "(/ 0 x)", "(/ 1 x)", "(+ x y)", "4", "x"],
                   "expr_simplify: lo que no se debe tocar")

    def test_keeps_the_value(self):
        self.lines(r"""
        Expr *e = need(ADD(MUL(ADD(V("x"), N(0)), SUB(V("y"), MUL(N(2), N(3)))), DIV(V("x"), N(1))));
        double before = own_eval(e, 2.0, 9.0);
        e = expr_simplify(e);
        if (e == NULL) fail("expr_simplify devolvió NULL");
        printf("%d\n", before == own_eval(e, 2.0, 9.0));
        dump(e);
        printf("\n");
        expr_free(e);
        """, ["1", "(+ (* x (- y 6)) x)"], "expr_simplify: el valor no cambia")


class Derive(LabCase):
    def test_leaves(self):
        self.lines(r"""
        Expr *e = need(N(5));
        Expr *d = expr_derive(e, "x");
        dump(d); printf("\n"); if (d) expr_free(d);
        expr_free(e);
        e = need(V("x"));
        d = expr_derive(e, "x");
        dump(d); printf("\n"); if (d) expr_free(d);
        d = expr_derive(e, "y");
        dump(d); printf("\n"); if (d) expr_free(d);
        expr_free(e);
        """, ["0", "1", "0"], "expr_derive de una hoja")

    def test_sum_and_difference(self):
        self.ok(r"""
        check_derivative(ADD(V("x"), N(3)), 2.0, 1.0);
        check_derivative(SUB(N(3), V("x")), 2.0, 1.0);
        check_derivative(ADD(V("x"), SUB(V("x"), V("y"))), 2.0, 1.0);
        """, "expr_derive de + y -")

    def test_product(self):
        self.ok(r"""
        check_derivative(MUL(V("x"), V("x")), 3.0, 1.0);
        check_derivative(MUL(N(3), V("x")), 2.0, 1.0);
        check_derivative(MUL(V("x"), V("y")), 2.0, 5.0);
        check_derivative(MUL(ADD(V("x"), N(1)), SUB(V("x"), N(2))), 1.5, 1.0);
        check_derivative(MUL(MUL(V("x"), V("x")), V("x")), -2.0, 1.0);
        """, "expr_derive de *")

    def test_quotient(self):
        self.ok(r"""
        check_derivative(DIV(V("x"), N(2)), 3.0, 1.0);
        check_derivative(DIV(N(1), V("x")), 2.0, 1.0);
        check_derivative(DIV(V("x"), ADD(V("x"), N(1))), 2.0, 1.0);
        check_derivative(DIV(MUL(V("x"), V("x")), SUB(V("x"), V("y"))), 4.0, 1.0);
        """, "expr_derive de /")

    def test_shares_nothing_and_changes_nothing(self):
        self.lines(r"""
        Expr *e = need(DIV(MUL(V("x"), ADD(V("x"), N(1))), SUB(V("x"), N(2))));
        Expr *d = expr_derive(e, "x");
        if (d == NULL) fail("expr_derive devolvió NULL");
        dump(e);
        printf("\n");
        expr_free(e);
        double value = own_eval(d, 3.0, 0.0);
        printf("%d\n", isfinite(value));
        expr_free(d);
        """, ["(/ (* x (+ x 1)) (- x 2))", "1"], "expr_derive: no cambia ni comparte")

    def test_no_memory(self):
        self.ok(r"""
        Expr *e = need(DIV(MUL(V("x"), ADD(V("y"), N(1))), SUB(V("x"), N(2))));
        EACH_FAILURE(d, expr_derive(e, "x"))
        expr_free(e);
        """, "expr_derive sin memoria", wrap=True)


# ---------------------------------------------------------------- Lo demás


class Whole(LabCase):
    def test_compiles_without_warnings(self):
        with tempfile.TemporaryDirectory() as name:
            done = subprocess.run(
                ["gcc", *FLAGS, f"-I{LAB}", "-c", str(source("expr.c")), "-o", str(pathlib.Path(name) / "expr.o")],
                text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
            )
        self.assertEqual(done.returncode, 0, done.stdout)
        self.assertEqual(done.stdout, "", f"expr.c da advertencias:\n{done.stdout}")

    def test_no_globals_no_goto(self):
        path = source("expr.c")
        text = path.read_text(encoding="utf-8")
        self.assertIsNone(re.search(r"\bgoto\b", text), "expr.c usa goto.")
        with tempfile.TemporaryDirectory() as name:
            obj = pathlib.Path(name) / "expr.o"
            subprocess.run(["gcc", "-std=c17", f"-I{LAB}", "-c", str(path), "-o", str(obj)], check=True)
            done = subprocess.run(["nm", str(obj)], text=True, stdout=subprocess.PIPE, check=True)
        found = [line.split()[-1] for line in done.stdout.splitlines() if re.search(r" [bBdD] ", line)]
        self.assertEqual(found, [], f"expr.c tiene variables globales: {found}")

    def program(self, main_file):
        with tempfile.TemporaryDirectory() as name:
            binary = pathlib.Path(name) / "program"
            built = subprocess.run(
                ["gcc", *FLAGS, f"-I{LAB}", str(source(main_file)), *expr_sources(), "-lm", "-o", str(binary)],
                text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
            )
            self.assertEqual(built.returncode, 0, built.stdout)
            done = subprocess.run([str(binary)], text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                                  encoding="utf-8", errors="replace", timeout=60)
        return done.stdout, done.returncode

    def test_expr_tests_pass(self):
        out, code = self.program("expr_tests.c")
        self.assertEqual(code, 0, f"expr_tests falla: {explain(out, code)}")

    def test_algebra_agrees_with_the_numbers(self):
        out, code = self.program("algebra.c")
        self.assertEqual(code, 0, f"algebra falla: {explain(out, code)}")
        self.assertNotIn("MAL", out)
        self.assertEqual(out.count(" bien"), 11, out)


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-13.md"
SLIDES = ROOT / "slides" / "week-13.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)
    # El número de cada marco cambia entre aarch64 y x86_64.
    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):
                # Lo que usa la solución solo se comprueba si está disponible.
                if "build/solution/" in command and not (HERE / "solution").exists():
                    continue
                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|codigomicro"):
            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/algebra.cDescargar lab/algebra.c
// Junta todo el módulo: escribe, evalúa, copia, simplifica y deriva unas expresiones.
// La derivada se comprueba contra una aproximación numérica.
#include <math.h>
#include <stdio.h>

#include "expr.h"

#define STEP 0.00001

// Evalúa e con name = x. Devuelve 1 si pudo.
static int eval_at(const Expr *e, const char *name, double x, double *result) {
    Env *env = env_new(NULL);
    if (env == NULL) {
        return 0;
    }
    int ok = env_set(env, name, x) && expr_eval(e, env, result);
    env_free(env);
    return ok;
}

// La pendiente entre dos puntos muy cercanos a x. Es casi igual a la derivada en x.
static int numeric_derivative(const Expr *e, const char *name, double x, double *result) {
    double before;
    double after;
    if (!eval_at(e, name, x - STEP, &before) || !eval_at(e, name, x + STEP, &after)) {
        return 0;
    }
    *result = (after - before) / (2 * STEP);
    return 1;
}

static void compare_derivative(const Expr *e, const Expr *derivative, double x) {
    double exact;
    double approximate;
    if (eval_at(derivative, "x", x, &exact) && numeric_derivative(e, "x", x, &approximate)) {
        printf("  x = %-3g derivada = %-9.4f numérica = %-9.4f %s\n", x, exact, approximate,
               fabs(exact - approximate) < 0.0001 ? "bien" : "MAL");
    } else {
        printf("  x = %-3g no se puede evaluar\n", x);
    }
}

// Se queda con e. Devuelve 1 si todo se pudo construir.
static int show(Expr *e) {
    if (e == NULL) {
        printf("no se pudo construir la expresión\n");
        return 0;
    }
    Expr *copy = expr_copy(e);
    Expr *derivative = expr_derive(e, "x");
    if (copy == NULL || derivative == NULL) {
        printf("no se pudo copiar o derivar la expresión\n");
        expr_free(copy);
        expr_free(derivative);
        expr_free(e);
        return 0;
    }

    printf("expresión:    ");
    expr_print(e, stdout);
    printf("\ncopia igual:  %s\n", expr_equal(e, copy) == 1 ? "sí" : "no");
    printf("derivada:     ");
    expr_print(derivative, stdout);
    derivative = expr_simplify(derivative);
    printf("\nsimplificada: ");
    if (derivative != NULL) {
        expr_print(derivative, stdout);
    }
    printf("\n");
    if (derivative != NULL) {
        compare_derivative(e, derivative, 0.0);
        compare_derivative(e, derivative, 1.0);
        compare_derivative(e, derivative, 2.5);
    }

    expr_free(derivative);
    expr_free(copy);
    expr_free(e);
    return 1;
}

int main(void) {
    // (x + 1) * (x - 2)
    int ok = show(expr_binary(OP_MUL, expr_binary(OP_ADD, expr_variable("x"), expr_number(1.0)),
                              expr_binary(OP_SUB, expr_variable("x"), expr_number(2.0))));
    printf("\n");
    // 3 * x * x + 2 * x + 7
    ok = show(expr_binary(OP_ADD,
                          expr_binary(OP_ADD,
                                      expr_binary(OP_MUL, expr_binary(OP_MUL, expr_number(3.0), expr_variable("x")),
                                                  expr_variable("x")),
                                      expr_binary(OP_MUL, expr_number(2.0), expr_variable("x"))),
                          expr_number(7.0))) && ok;
    printf("\n");
    // x / (x + 1)
    ok = show(expr_binary(OP_DIV, expr_variable("x"),
                          expr_binary(OP_ADD, expr_variable("x"), expr_number(1.0)))) && ok;
    printf("\n");
    // 1 / x: en x = 0 no se puede evaluar
    ok = show(expr_binary(OP_DIV, expr_number(1.0), expr_variable("x"))) && ok;
    return ok ? 0 : 1;
}
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_tests.cDescargar lab/expr_tests.c
// Pruebas de expr con assert.
// Agreguen sus casos en cada función de prueba. Corran con: build/lab/expr_tests
#include <assert.h>
#include <stdio.h>
#include <string.h>

#include "expr.h"

// Escribe la expresión en un archivo temporal y la lee de vuelta en text.
static void print_to_text(const Expr *e, char *text, int size) {
    FILE *file = tmpfile();
    assert(file != NULL);
    expr_print(e, file);
    rewind(file);
    if (fgets(text, size, file) == NULL) {
        text[0] = '\0';
    }
    fclose(file);
}

static void test_constructors(void) {
    Expr *e = expr_binary(OP_ADD, expr_number(2.0), expr_variable("x"));
    assert(e != NULL);
    assert(e->kind == EXPR_BINARY);
    assert(e->as.binary.op == OP_ADD);
    assert(e->as.binary.left->kind == EXPR_NUMBER);
    assert(e->as.binary.left->as.number == 2.0);
    assert(e->as.binary.right->kind == EXPR_VARIABLE);
    assert(strcmp(e->as.binary.right->as.name, "x") == 0);
    expr_free(e);
}

static void test_print(void) {
    char text[100];
    // (2 + 3) * x
    Expr *e = expr_binary(OP_MUL, expr_binary(OP_ADD, expr_number(2.0), expr_number(3.0)),
                          expr_variable("x"));
    assert(e != NULL);
    print_to_text(e, text, sizeof text);
    assert(strcmp(text, "(2 + 3) * x") == 0);
    expr_free(e);
}

static void test_eval(void) {
    Env *env = env_new(NULL);
    assert(env != NULL);
    assert(env_set(env, "x", 4.0));
    // 2 + 3 * x
    Expr *e = expr_binary(OP_ADD, expr_number(2.0),
                          expr_binary(OP_MUL, expr_number(3.0), expr_variable("x")));
    assert(e != NULL);
    double value = 0.0;
    assert(expr_eval(e, env, &value) == 1);
    assert(value == 14.0);
    expr_free(e);
    env_free(env);
}

// Sesión 2: escriban una función de prueba por cada función nueva, y llámenlas desde main.

int main(void) {
    test_constructors();
    test_print();
    test_eval();

    printf("Todas las pruebas pasaron\n");
    return 0;
}
lab/expr.cDescargar lab/expr.c
#include "expr.h"

#include <stdlib.h>
#include <string.h>

// --- Por escribir. Lo que debe hacer cada una está en expr.h. ---
// Pueden agregar funciones auxiliares static. Sin variables globales.

// Sesión 1

Expr *expr_number(double value) {
    (void) value;
    return NULL;
}

Expr *expr_variable(const char *name) {
    (void) name;
    return NULL;
}

Expr *expr_binary(BinaryOp op, Expr *left, Expr *right) {
    (void) op;
    (void) left;
    (void) right;
    return NULL;
}

void expr_free(Expr *e) {
    (void) e;
}

void expr_print(const Expr *e, FILE *out) {
    (void) e;
    (void) out;
}

int expr_eval(const Expr *e, const Env *env, double *result) {
    (void) e;
    (void) env;
    (void) result;
    return 0;
}

// Sesión 2

Expr *expr_copy(const Expr *e) {
    (void) e;
    return NULL;
}

int expr_equal(const Expr *a, const Expr *b) {
    (void) a;
    (void) b;
    return -1;
}

Expr *expr_simplify(Expr *e) {
    (void) e;
    return NULL;
}

Expr *expr_derive(const Expr *e, const char *name) {
    (void) e;
    (void) name;
    return NULL;
}
lab/expr.hDescargar lab/expr.h
// Árboles de expresión: número, variable y operación binaria.
//
// 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 } 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 -, y que todos 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.
// Ejemplos: 2 + 3 * x     (2 + 3) * x     1 - 2 - 3     1 - (2 - 3)     1 + (2 + 3)
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.
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
// 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, sin dejar nada reservado. No cambia e ni comparte nodos con él.
Expr *expr_derive(const Expr *e, const char *name);

#endif
lab/README.mdDescargar lab/README.md
# Laboratorio de la semana 13: álgebra de expresiones

Hay dos sesiones de 2 horas. Cada una está pensada para unos 90 minutos de trabajo. Los comandos se
corren desde `code/week-13`. Las ideas están en los apuntes, en la sección "El laboratorio, paso a paso".

| Archivo | Qué es | ¿Lo cambian? |
|---|---|---|
| `expr.h` | El tipo `Expr` y la interfaz de todo el módulo. Sus comentarios son el enunciado | No |
| `expr.c` | Las funciones de `Expr` | **Sí** |
| `env.h` | La interfaz de la tabla de variables `Env` | No |
| `env.c` | La tabla de variables. Funciona, y no hace falta leerla | No |
| `expr_tests.c` | Sus pruebas con `assert`. Ya trae tres, como modelo | **Sí** |
| `algebra.c` | Un programa que usa todo el módulo y compara cada derivada con un cálculo numérico | No |

```sh
make lab                  # compila todo en build/lab/
make test-lab             # comprueba todo el laboratorio; falla hasta que terminen
```

`make test-lab` corre pruebas de las dos sesiones. Se puede correr un grupo solo:

```sh
LAB_DIR=lab python3 -m unittest test_lab.Constructors
```

Los grupos, en orden, son `Constructors`, `Free`, `Print` y `Eval` para la sesión 1, y `Copy`, `Equal`,
`Simplify` y `Derive` para la sesión 2. `Whole` comprueba al final que no haya advertencias ni variables
globales, y que `expr_tests` y `algebra` terminen bien.

Todo se compila con ASan y UBSan: una fuga hace fallar la prueba, aunque el resultado sea correcto. Varias
pruebas hacen fallar `malloc` a propósito, una reserva a la vez, para revisar sus caminos de error. Por eso,
**toda reserva se revisa, y al fallar no deja nada reservado**. En `-std=c17` no existe `strdup`: la copia del
nombre se hace con `malloc` y `strcpy`, como en la semana 7.

## Sesión 1: construir, liberar, escribir y evaluar

### Parte 1: leer (unos 15 minutos)

Lean `expr.h` completo, `env.h` y `expr_tests.c`. No hace falta abrir `env.c`. Respondan por escrito, sin
ejecutar nada:

1. Dibujen el árbol de `(1 + 2) * x`, con el contenido de cada nodo. ¿Cuántos nodos tiene? ¿Cuántas llamadas
   a `malloc` hará construirlo?
2. En `expr_tests.c`, `test_constructors` anida llamadas y solo revisa la última. ¿Por qué eso no deja fugas
   si un `expr_number` de adentro devuelve `NULL`?
3. ¿Por qué `expr_eval` devuelve un `int` y guarda el valor en `*result`, en vez de devolver el `double`?

### Parte 2: constructores y `expr_free` (unos 25 minutos)

Escriban `expr_number`, `expr_variable`, `expr_binary` y `expr_free`. Una función auxiliar `static` que reserve el
nodo y ponga la etiqueta les ahorra repetir código.

```sh
LAB_DIR=lab python3 -m unittest test_lab.Constructors test_lab.Free
```

### Parte 3: `expr_print` (unos 25 minutos)

Escriban `expr_print`. Las reglas de paréntesis están en `expr.h`, y estos casos son la prueba de que las entendieron:

| Árbol | Se escribe |
|---|---|
| suma de 2 y el producto de 3 y `x` | `2 + 3 * x` |
| producto de la suma de 2 y 3, y `x` | `(2 + 3) * x` |
| resta de la resta de 1 y 2, y 3 | `1 - 2 - 3` |
| resta de 1, y la resta de 2 y 3 | `1 - (2 - 3)` |
| suma de 1, y la suma de 2 y 3 | `1 + (2 + 3)` |

Escriban en `out`, no en la pantalla. Usen `fprintf` y `fputs`.

```sh
LAB_DIR=lab python3 -m unittest test_lab.Print
```

### Parte 4: `expr_eval` (unos 25 minutos)

Escriban `expr_eval` a partir del pseudocódigo de los apuntes. Usen `env_get`, y **no toquen `*result` si hay un
error**: cada lado se evalúa en una variable local. Corran `make lab` y luego `build/lab/expr_tests`, que debe
decir que todas las pruebas pasaron.

```sh
LAB_DIR=lab python3 -m unittest test_lab.Eval
```

**Si no alcanza el tiempo:** lo que falte de esta sesión lo terminan antes de la siguiente, porque la sesión 2
construye sobre todo esto.

## Sesión 2: copiar, comparar, simplificar y derivar

### Parte 1: `expr_copy` y `expr_equal` (unos 20 minutos)

Escriban las dos. Agreguen a `expr_tests.c` una función de prueba que copie un árbol, compruebe que las dos
versiones son iguales, cambie un número de la copia y compruebe que ya no lo son. ¿Por qué `expr_copy` queda
tan corta si usan bien `expr_binary`?

```sh
LAB_DIR=lab python3 -m unittest test_lab.Copy test_lab.Equal
```

### Parte 2: `expr_simplify` (unos 35 minutos)

Antes de escribir, respondan: al aplicar `x * 1`, ¿qué nodos se liberan, y cuál se devuelve? ¿Y al aplicar
`x * 0`?

Las reglas están en `expr.h`. Simplifiquen primero a los hijos, y guarden lo que devuelven: un hijo puede haber
cambiado de dirección. Cuiden el orden: revisen `x * 0` antes de operar constantes, para que `-3 * 0` dé `0` y no
`-0`.

No agreguen reglas como `0 - x` o `0 / x`: una cambia el resultado, y la otra cambia cuándo hay un error.

```sh
LAB_DIR=lab python3 -m unittest test_lab.Simplify
```

### Parte 3: `expr_derive` (unos 35 minutos)

Las reglas están en los apuntes. La idea que hay que cuidar: un nodo tiene un solo padre. Si la regla usa `g` dos
veces, hay dos copias. `e` no se cambia.

```sh
make lab
build/lab/algebra
LAB_DIR=lab python3 -m unittest test_lab.Derive
make test-lab
```

`algebra` escribe la derivada de cuatro expresiones y la compara con la pendiente calculada entre dos puntos muy
cercanos. Cada línea debe decir `bien`. Su derivada puede tener otra forma que la de los apuntes, por ejemplo con
los términos en otro orden. Lo que debe coincidir es el valor.

**Si no alcanza el tiempo:** el caso de `/` en `expr_derive` y la revisión con `algebra` son lo que se puede
terminar antes de la próxima clase.

## Si sobra tiempo

1. Agreguen una regla a `expr_simplify` que convierta `x - x` en `0` cuando los dos lados sean iguales, usando
   `expr_equal`. Busquen un ejemplo en el que eso cambie lo que hace `expr_eval`.
2. Escriban `int expr_count(const Expr *e)` y úsenla en `algebra.c` para ver cuántos nodos ahorra simplificar.
3. Escriban `Expr *expr_substitute(const Expr *e, const char *name, const Expr *value)`, que devuelve un árbol nuevo
   donde cada `name` se cambió por una copia de `value`. ¿Cuántas copias de `value` se hacen?

Compilación

Cómo se compilan y ejecutan los ejemplos.

MakefileDescargar Makefile
# Semana 13: datos recursivos. 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_demo logic_free_order logic_simplify_demo node_sizes env_demo

LAB_HEADERS = lab/expr.h lab/env.h
LAB_PROGRAMS = expr_tests algebra

.PHONY: all lab solution test test-lab clean

all: $(PROGRAMS:%=$(BUILD)/%)

$(BUILD)/logic_demo: logic_demo.c logic.c logic.h | $(BUILD)
	$(CC) $(CFLAGS) logic_demo.c logic.c -o $@

$(BUILD)/logic_free_order: logic_free_order.c logic.c logic.h | $(BUILD)
	$(CC) $(CFLAGS) logic_free_order.c logic.c -o $@

$(BUILD)/logic_simplify_demo: logic_simplify_demo.c logic.c logic.h | $(BUILD)
	$(CC) $(CFLAGS) logic_simplify_demo.c logic.c -o $@

$(BUILD)/node_sizes: node_sizes.c logic.h $(LAB_HEADERS) | $(BUILD)
	$(CC) $(CFLAGS) -Ilab node_sizes.c -o $@

$(BUILD)/env_demo: env_demo.c lab/env.c lab/env.h | $(BUILD)
	$(CC) $(CFLAGS) -Ilab env_demo.c lab/env.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/expr_tests: $(BUILD)/lab/expr_tests.o $(BUILD)/lab/expr.o $(BUILD)/lab/env.o
	$(CC) $(CFLAGS) $^ -o $@

# algebra usa fabs y por eso se enlaza con la biblioteca matemática.
$(BUILD)/lab/algebra: $(BUILD)/lab/algebra.o $(BUILD)/lab/expr.o $(BUILD)/lab/env.o
	$(CC) $(CFLAGS) $^ -lm -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/expr_tests: $(BUILD)/solution/expr_tests.o $(BUILD)/solution/expr.o $(BUILD)/solution/env.o
	$(CC) $(CFLAGS) $^ -o $@

$(BUILD)/solution/algebra: $(BUILD)/solution/algebra.o $(BUILD)/solution/expr.o $(BUILD)/solution/env.o
	$(CC) $(CFLAGS) $^ -lm -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
	@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__