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 * xes 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:
- Leer el encabezado dado de
Expr, la tabla de variablesEnvy las pruebas. - Escribir los constructores,
expr_freeyexpr_printcon los paréntesis necesarios. - Escribir
expr_eval, usandoEnvpor 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
- Dibujen el árbol de
(1 + 2) * x. ¿Cuántos nodos tiene? - ¿Por qué
expr_freelibera primero a los hijos? - ¿Qué devuelve
expr_evalsi la variable no existe? ¿Cómo diseñarían ese error? - ¿Por qué
x * 0 → 0necesita liberar el subárbol dex?
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.
// 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;
}// 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;
}// 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;
}// 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;
}#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;
}// 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// 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.
// 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;
}// 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;
}// 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;
}// 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;
}// 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;
}// 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.
"""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()"""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()"""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.
// 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;
}#include "env.h"
#include <stdlib.h>
#include <string.h>
struct binding {
char *name;
double value;
struct binding *next;
};
struct env {
Env *parent;
struct binding *first;
};
Env *env_new(Env *parent) {
Env *env = malloc(sizeof *env);
if (env == NULL) {
return NULL;
}
env->parent = parent;
env->first = NULL;
return env;
}
void env_free(Env *env) {
if (env == NULL) {
return;
}
struct binding *current = env->first;
while (current != NULL) {
struct binding *next = current->next;
free(current->name);
free(current);
current = next;
}
free(env);
}
int env_set(Env *env, const char *name, double value) {
for (struct binding *b = env->first; b != NULL; b = b->next) {
if (strcmp(b->name, name) == 0) {
b->value = value;
return 1;
}
}
struct binding *b = malloc(sizeof *b);
if (b == NULL) {
return 0;
}
b->name = malloc(strlen(name) + 1);
if (b->name == NULL) {
free(b);
return 0;
}
strcpy(b->name, name);
b->value = value;
b->next = env->first;
env->first = b;
return 1;
}
int env_get(const Env *env, const char *name, double *value) {
for (const Env *e = env; e != NULL; e = e->parent) {
for (const struct binding *b = e->first; b != NULL; b = b->next) {
if (strcmp(b->name, name) == 0) {
*value = b->value;
return 1;
}
}
}
return 0;
}// Tabla de variables de un marco. Se usa solo por esta interfaz.
//
// Owner: quien llama a env_new libera con env_free. env_free no libera al padre.
#ifndef ENV_H
#define ENV_H
typedef struct env Env;
// Crea un marco vacío. parent puede ser NULL. Devuelve NULL si no hay memoria.
Env *env_new(Env *parent);
// Libera el marco y sus nombres. Acepta NULL.
void env_free(Env *env);
// Define name en este marco, o cambia su valor si ya está en este marco.
// Copia el nombre. Devuelve 1 si pudo y 0 si no hay memoria.
int env_set(Env *env, const char *name, double value);
// Busca name en este marco y luego en sus padres. Si lo encuentra, guarda el
// valor en *value y devuelve 1. Si no, devuelve 0 y no toca *value.
int env_get(const Env *env, const char *name, double *value);
#endif// 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;
}#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;
}// Á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# 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.
# 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__