Semana 7 — Tipos

Objetivos

  • Definir tipos que expresen el problema, con typedef, enum y struct.
  • Representar un valor de varias formas con una unión etiquetada.
  • Reconocer conversiones peligrosas y comportamiento indefinido.

Teoría — Tipos: typedef, enum, uniones etiquetadas · ✂️ Conversiones · Comportamiento indefinido

Lectura: Apuntes, semana 7.

Temas:

  • Un tipo dice qué valores existen y qué se puede hacer con ellos. Elegir bien el tipo evita bugs.
  • typedef, enum en vez de números mágicos, y switch con revisión de -Wall.
  • union y unión etiquetada: una etiqueta enum dice cuál variante está activa. Hacer que los estados inválidos no se puedan representar.
  • ✂️ Conversiones implícitas, promoción de enteros, comparar con signo y sin signo, size_t.
  • Comportamiento indefinido: overflow con signo, índice fuera de rango y variable sin inicializar. Por qué “me funcionó” no prueba nada. -fsanitize=undefined.

Laboratorio — Un valor con varios tipos

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

Sesión 1:

  1. Escribir el módulo Value, una unión etiquetada que guarda un entero, un decimal, una cadena o nada, con constructores, value_print, value_equal y value_free.
  2. Usarlo para leer un archivo con columnas de distinto tipo y calcular un resumen. Probar sin fugas.

Sesión 2: Práctica 2.

Entregas

Preguntas de repaso

  1. ¿Qué ventaja tiene un enum frente a #define RED 0?
  2. ¿Qué pasa si leen v.as.number cuando la etiqueta dice que hay una cadena?
  3. ¿Qué escribe if (-1 < 1u) puts("menor"); else puts("mayor");? ¿Por qué?
  4. ¿Por qué un programa con comportamiento indefinido puede funcionar hoy y fallar mañana?

Código de la semana

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

Ejemplos (12)Errores (14)Pruebas (3)Laboratorio (9)Compilación (1)

Ejemplos

Los programas que usan los apuntes y la clase.

enum_values.cDescargar enum_values.c
// Por dentro, un enum es un entero, y C no revisa su rango.
#include <stdio.h>

enum trip_state { TRIP_WAITING, TRIP_ON_ROUTE, TRIP_FINISHED };
enum fare { FARE_STUDENT = 100, FARE_ADULT = 250, FARE_NIGHT };

int main(void) {
    printf("%d %d %d\n", TRIP_WAITING, TRIP_ON_ROUTE, TRIP_FINISHED);
    printf("%d %d %d\n", FARE_STUDENT, FARE_ADULT, FARE_NIGHT);
    printf("sizeof(enum trip_state) = %zu\n", sizeof(enum trip_state));

    enum trip_state state = 7;
    printf("state = %d\n", state);

    int total = TRIP_FINISHED + FARE_ADULT;
    printf("total = %d\n", total);
    return 0;
}
narrowing.cDescargar narrowing.c
// Conversiones a un tipo más chico, que -Wall -Wextra no avisa.
#include <stdio.h>

int main(void) {
    int total = 300;
    unsigned char small = total;
    printf("small = %d\n", small);

    double price = 3.99;
    int soles = price;
    printf("soles = %d\n", soles);

    unsigned int large = 3000000000u;
    int wrong = large;
    printf("wrong = %d\n", wrong);
    return 0;
}
payment_demo.cDescargar payment_demo.c
// Tres pagos de distinta forma en un mismo arreglo.
#include <stdio.h>

#include "payment.h"

#define PAYMENTS 3

int main(void) {
    Payment payments[PAYMENTS] = {
        payment_cash(2.5),
        payment_card(4821),
        payment_wallet("987654321"),
    };

    for (int i = 0; i < PAYMENTS; i++) {
        payment_print(&payments[i]);
    }

    for (int i = 0; i < PAYMENTS; i++) {
        payment_free(&payments[i]);
    }
    return 0;
}
payment_sizes.cDescargar payment_sizes.c
// Lo que ocupa un pago con todos sus campos, y lo que ocupa con una union.
#include <stdio.h>

#include "payment.h"

struct loose_payment {
    int kind;
    double cash;
    int card_end;
    char *phone;
};

int main(void) {
    printf("sizeof(struct loose_payment) = %zu\n", sizeof(struct loose_payment));
    printf("sizeof(Payment) = %zu\n", sizeof(Payment));

    struct loose_payment bad = {0, 2.5, 4821, NULL};
    printf("kind = %d, cash = %.2f, card_end = %d\n", bad.kind, bad.cash, bad.card_end);
    return 0;
}
payment.cDescargar payment.c
#include "payment.h"

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

Payment payment_cash(double soles) {
    Payment p;
    p.kind = PAY_CASH;
    p.as.cash = soles;
    return p;
}

Payment payment_card(int card_end) {
    Payment p;
    p.kind = PAY_CARD;
    p.as.card_end = card_end;
    return p;
}

Payment payment_wallet(const char *phone) {
    char *copy = malloc(strlen(phone) + 1);
    if (copy == NULL) {
        return payment_cash(0.0);
    }
    strcpy(copy, phone);

    Payment p;
    p.kind = PAY_WALLET;
    p.as.phone = copy;
    return p;
}

void payment_print(const Payment *p) {
    switch (p->kind) {
    case PAY_CASH:
        printf("efectivo: S/ %.2f\n", p->as.cash);
        break;
    case PAY_CARD:
        printf("tarjeta terminada en %04d\n", p->as.card_end);
        break;
    case PAY_WALLET:
        printf("billetera del %s\n", p->as.phone);
        break;
    }
}

void payment_free(Payment *p) {
    if (p->kind == PAY_WALLET) {
        free(p->as.phone);
    }
    *p = payment_cash(0.0);
}
payment.hDescargar payment.h
// Un pago de pasaje: en efectivo, con tarjeta o con billetera del celular.
#ifndef PAYMENT_H
#define PAYMENT_H

typedef enum { PAY_CASH, PAY_CARD, PAY_WALLET } PaymentKind;

typedef struct {
    PaymentKind kind;
    union {
        double cash;   // PAY_CASH: soles recibidos
        int card_end;  // PAY_CARD: últimos cuatro dígitos
        char *phone;   // PAY_WALLET: número del celular; lo posee el Payment
    } as;
} Payment;

Payment payment_cash(double soles);
Payment payment_card(int card_end);

// Guarda una copia de phone. Si no hay memoria, devuelve un pago en efectivo de 0 soles.
Payment payment_wallet(const char *phone);

void payment_print(const Payment *p);

// Libera lo que el pago posee y lo deja como un pago en efectivo de 0 soles.
void payment_free(Payment *p);

#endif
promotion.cDescargar promotion.c
// Antes de operar, lo que es más chico que un int se convierte a int.
#include <stdio.h>

int main(void) {
    unsigned char a = 200;
    unsigned char b = 100;

    int sum = a + b;
    unsigned char small_sum = a + b;
    printf("sum = %d, small_sum = %d\n", sum, small_sum);
    printf("sizeof(a) = %zu, sizeof(a + b) = %zu\n", sizeof(a), sizeof(a + b));

    unsigned char top = 255;
    printf("top + 1 == 0: %d\n", top + 1 == 0);

    unsigned int big_top = 4294967295u;
    printf("big_top + 1 == 0: %d\n", big_top + 1 == 0);
    return 0;
}
sign_rule.cDescargar sign_rule.c
// Lo que queda de un -1 cuando C lo convierte a un tipo sin signo.
#include <stdio.h>
#include <string.h>

int main(void) {
    int negative = -1;

    printf("como unsigned int: %u\n", (unsigned int) negative);
    printf("como size_t: %zu\n", (size_t) negative);
    printf("con cast a int: %d\n", negative < (int) strlen("hola"));
    return 0;
}
trip_defines.cDescargar trip_defines.c
// Estados de un viaje con números mágicos y #define.
#include <stdio.h>

#define TRIP_WAITING 0
#define TRIP_ON_ROUTE 1
#define TRIP_FINISHED 2

const char *state_name(int state) {
    if (state == TRIP_WAITING) {
        return "esperando";
    }
    if (state == TRIP_ON_ROUTE) {
        return "en ruta";
    }
    return "terminado";
}

int main(void) {
    int state = TRIP_ON_ROUTE;
    printf("%d: %s\n", state, state_name(state));

    state = 7;
    printf("%d: %s\n", state, state_name(state));
    return 0;
}
trip_enum.cDescargar trip_enum.c
// Estados de un viaje con enum y switch.
#include <stdio.h>

enum trip_state { TRIP_WAITING, TRIP_ON_ROUTE, TRIP_FINISHED };

const char *state_name(enum trip_state state) {
    switch (state) {
    case TRIP_WAITING:
        return "esperando";
    case TRIP_ON_ROUTE:
        return "en ruta";
    case TRIP_FINISHED:
        return "terminado";
    }
    return "desconocido";
}

enum trip_state next_state(enum trip_state state) {
    if (state == TRIP_WAITING) {
        return TRIP_ON_ROUTE;
    }
    return TRIP_FINISHED;
}

int main(void) {
    enum trip_state state = TRIP_WAITING;
    printf("%s\n", state_name(state));

    state = next_state(state);
    printf("%s\n", state_name(state));

    state = next_state(state);
    printf("%s\n", state_name(state));
    return 0;
}
typedef_units.cDescargar typedef_units.c
// typedef da otro nombre a un tipo. No crea un tipo distinto.
#include <stdio.h>

typedef int Meters;
typedef int Seconds;

double speed(Meters distance, Seconds time) {
    return (double) distance / time;
}

int main(void) {
    Meters distance = 1000;
    Seconds time = 50;

    printf("bien: %.2f m/s\n", speed(distance, time));
    printf("al revés: %.2f m/s\n", speed(time, distance));
    return 0;
}
union_bytes.cDescargar union_bytes.c
// Los campos de una union empiezan en el mismo lugar de la memoria.
#include <stdio.h>

struct both {
    int integer;
    double decimal;
};

union either {
    int integer;
    double decimal;
};

int main(void) {
    printf("sizeof(struct both) = %zu\n", sizeof(struct both));
    printf("sizeof(union either) = %zu\n", sizeof(union either));

    union either u;
    u.integer = 7;
    printf("guardé un int: %d\n", u.integer);

    u.decimal = 3.14;
    printf("guardé un double: %.2f\n", u.decimal);
    printf("leído como int: %d\n", u.integer);
    return 0;
}

Errores

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

errors/default_hides.cDescargar errors/default_hides.c
// Compila sin avisos: el default le quita al compilador la lista de casos por revisar.
#include <stdio.h>

enum trip_state { TRIP_WAITING, TRIP_ON_ROUTE, TRIP_FINISHED, TRIP_CANCELLED };

const char *state_name(enum trip_state state) {
    switch (state) {
    case TRIP_WAITING:
        return "esperando";
    case TRIP_ON_ROUTE:
        return "en ruta";
    case TRIP_FINISHED:
        return "terminado";
    default:
        return "desconocido";
    }
}

int main(void) {
    printf("%s\n", state_name(TRIP_CANCELLED));
    return 0;
}
errors/double_too_big.cDescargar errors/double_too_big.c
// Comportamiento indefinido: la parte entera del double no cabe en un int.
#include <stdio.h>

int main(void) {
    double x;
    if (scanf("%lf", &x) != 1) {
        return 1;
    }
    int n = x;
    printf("n = %d\n", n);
    return 0;
}
errors/enum_mix.cDescargar errors/enum_mix.c
// Advertencia al mezclar dos enum. Con un int, en cambio, no hay ninguna.
#include <stdio.h>

enum trip_state { TRIP_WAITING, TRIP_ON_ROUTE, TRIP_FINISHED };
enum seat { SEAT_WINDOW, SEAT_AISLE };

void show(enum trip_state state, int passengers) {
    printf("estado %d, %d pasajeros\n", state, passengers);
}

int main(void) {
    show(SEAT_AISLE, 20);
    show(20, TRIP_ON_ROUTE);
    return 0;
}
errors/index_out.cDescargar errors/index_out.c
// Comportamiento indefinido: se lee fuera del arreglo si el índice no es válido.
#include <stdio.h>

int main(void) {
    int grades[4] = {14, 9, 17, 11};
    int i;
    if (scanf("%d", &i) != 1) {
        return 1;
    }
    printf("grades[%d] = %d\n", i, grades[i]);
    return 0;
}
errors/minus_one.cDescargar errors/minus_one.c
// Compara un int con un unsigned int: la pregunta de repaso de la semana.
#include <stdio.h>

int main(void) {
    if (-1 < 1u) {
        puts("menor");
    } else {
        puts("mayor");
    }
    return 0;
}
errors/missing_case.cDescargar errors/missing_case.c
// Advertencia: el switch no atiende una de las etiquetas del enum.
#include <stdio.h>

enum trip_state { TRIP_WAITING, TRIP_ON_ROUTE, TRIP_FINISHED, TRIP_CANCELLED };

const char *state_name(enum trip_state state) {
    switch (state) {
    case TRIP_WAITING:
        return "esperando";
    case TRIP_ON_ROUTE:
        return "en ruta";
    case TRIP_FINISHED:
        return "terminado";
    }
    return "desconocido";
}

int main(void) {
    printf("%s\n", state_name(TRIP_CANCELLED));
    return 0;
}
errors/no_final_return.cDescargar errors/no_final_return.c
// Advertencia: un enum puede guardar otros enteros, y el switch podría no retornar.
#include <stdio.h>

enum trip_state { TRIP_WAITING, TRIP_ON_ROUTE, TRIP_FINISHED };

const char *state_name(enum trip_state state) {
    switch (state) {
    case TRIP_WAITING:
        return "esperando";
    case TRIP_ON_ROUTE:
        return "en ruta";
    case TRIP_FINISHED:
        return "terminado";
    }
}

int main(void) {
    printf("%s\n", state_name(TRIP_WAITING));
    return 0;
}
errors/overflow_compare.cDescargar errors/overflow_compare.c
// Comportamiento indefinido: x + 1 no cabe en un int si x es el mayor int.
#include <stdio.h>

int main(void) {
    int x;
    if (scanf("%d", &x) != 1) {
        return 1;
    }

    int next = x + 1;
    printf("next = %d, next > x: %d\n", next, next > x);

    int steps = 0;
    for (int i = x; i > 0 && steps < 5; i++) {
        steps++;
    }
    printf("steps = %d\n", steps);
    return 0;
}
errors/shared_owner.cDescargar errors/shared_owner.c
// Copiar un Payment copia la dirección de su cadena: quedan dos owners y se libera dos veces.
#include "payment.h"

int main(void) {
    Payment first = payment_wallet("987654321");
    Payment second = first;

    payment_print(&second);

    payment_free(&first);
    payment_free(&second);
    return 0;
}
errors/size_compare.cDescargar errors/size_compare.c
// Compara un int negativo con lo que devuelve strlen.
#include <stdio.h>
#include <string.h>

int main(void) {
    const char *word = "hola";
    int position = -1;

    if (position < strlen(word)) {
        printf("la posición está antes del final\n");
    } else {
        printf("la posición está después del final\n");
    }
    return 0;
}
errors/struct_units.cDescargar errors/struct_units.c
// No compila: dos struct son tipos distintos, aunque tengan los mismos campos.
#include <stdio.h>

typedef struct { int value; } Meters;
typedef struct { int value; } Seconds;

double speed(Meters distance, Seconds time) {
    return (double) distance.value / time.value;
}

int main(void) {
    Meters distance = {1000};
    Seconds time = {50};

    printf("%.2f\n", speed(time, distance));
    return 0;
}
errors/uninitialized.cDescargar errors/uninitialized.c
// Comportamiento indefinido: percent no recibe valor para una edad entre 18 y 64.
#include <stdio.h>

int discount(int age) {
    int percent;
    if (age < 18) {
        percent = 50;
    } else if (age >= 65) {
        percent = 30;
    }
    return percent;
}

int main(void) {
    int age;
    if (scanf("%d", &age) != 1) {
        return 1;
    }
    printf("descuento: %d%%\n", discount(age));
    return 0;
}
errors/wrong_text.cDescargar errors/wrong_text.c
// Al revés: la etiqueta dice decimal, y sus bytes se usan como dirección de una cadena.
#include <stdio.h>

typedef enum { VALUE_NUMBER, VALUE_TEXT } Kind;

typedef struct {
    Kind kind;
    union {
        double number;
        const char *text;
    } as;
} Cell;

int main(void) {
    Cell v;
    v.kind = VALUE_NUMBER;
    v.as.number = 2.5;

    printf("v.as.text = %s\n", v.as.text);
    return 0;
}
errors/wrong_variant.cDescargar errors/wrong_variant.c
// Lee la variante equivocada: la etiqueta dice cadena y se lee un decimal.
#include <stdio.h>

typedef enum { VALUE_NUMBER, VALUE_TEXT } Kind;

typedef struct {
    Kind kind;
    union {
        double number;
        const char *text;
    } as;
} Cell;

int main(void) {
    Cell v;
    v.kind = VALUE_TEXT;
    v.as.text = "Callao";

    printf("texto: %s\n", v.as.text);
    printf("v.as.number = %g\n", v.as.number);
    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"]
PLAIN = ["typedef_units", "trip_defines", "trip_enum", "enum_values", "union_bytes", "payment_sizes",
         "sign_rule", "promotion", "narrowing"]


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


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


class Programs(unittest.TestCase):
    def test_every_program_compiles_without_warnings(self):
        for name in PLAIN + ["payment", "payment_demo"]:
            with self.subTest(program=name):
                subprocess.run(
                    ["gcc", *FLAGS, "-Werror", "-c", f"{name}.c", "-o", "/dev/null"],
                    cwd=HERE, check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                )

    def test_header_can_be_included_twice(self):
        with tempfile.TemporaryDirectory() as name:
            source = pathlib.Path(name) / "twice.c"
            source.write_text('#include "payment.h"\n#include "payment.h"\nint main(void) { return 0; }\n')
            subprocess.run(["gcc", *BASE, "-Werror", f"-I{HERE}", str(source), "-o", "/dev/null"], check=True)

    def test_typedef_does_not_protect_the_order(self):
        out, _ = run(BUILD / "typedef_units")
        self.assertEqual(out.splitlines(), ["bien: 20.00 m/s", "al revés: 0.05 m/s"])

    def test_union_is_as_big_as_its_largest_field(self):
        out, _ = run(BUILD / "union_bytes")
        self.assertIn("sizeof(union either) = 8", out)

    def test_payment_demo_has_no_leaks(self):
        out, code = run(BUILD / "payment_demo")
        self.assertEqual(code, 0, out)
        self.assertEqual(len(out.splitlines()), 3)


class CompilerClaims(unittest.TestCase):
    def messages(self, source, flags=BASE):
        _, folder, text, code = build(source, flags)
        folder.cleanup()
        return text, code

    def test_struct_units_do_not_compile(self):
        text, code = self.messages("errors/struct_units.c")
        self.assertNotEqual(code, 0)
        self.assertIn("incompatible type", text)

    def test_missing_case_warns_and_default_hides_it(self):
        self.assertIn("-Wswitch", self.messages("errors/missing_case.c")[0])
        self.assertEqual(self.messages("errors/default_hides.c")[0], "")

    def test_mixing_enums_warns_but_an_int_does_not(self):
        text, _ = self.messages("errors/enum_mix.c")
        self.assertEqual(text.count("-Wenum-conversion"), 1)

    def test_minus_one_warns_and_prints_mayor(self):
        binary, folder, text, _ = build("errors/minus_one.c", BASE)
        self.assertIn("-Wsign-compare", text)
        self.assertEqual(run(binary)[0].strip(), "mayor")
        folder.cleanup()

    def test_narrowing_needs_wconversion(self):
        self.assertEqual(self.messages("narrowing.c")[0], "")
        self.assertIn("-Wconversion", self.messages("narrowing.c", [*BASE, "-Wconversion"])[0])

    def test_these_errors_give_no_warning(self):
        for source in ("errors/overflow_compare.c", "errors/index_out.c", "errors/double_too_big.c",
                       "errors/wrong_variant.c", "errors/wrong_text.c"):
            with self.subTest(source=source):
                self.assertEqual(self.messages(source)[0], "")
        _, folder, text, _ = build("errors/shared_owner.c", BASE, ["payment.c"])
        folder.cleanup()
        self.assertEqual(text, "")

    def test_uninitialized_warns_only_with_optimization(self):
        self.assertEqual(self.messages("errors/uninitialized.c", [*BASE, "-O0"])[0], "")
        self.assertIn("-Wmaybe-uninitialized", self.messages("errors/uninitialized.c", [*BASE, "-O2"])[0])


class SanitizerClaims(unittest.TestCase):
    def test_wrong_variant_is_not_detected(self):
        binary, folder, _, _ = build("errors/wrong_variant.c", FLAGS)
        out, code = run(binary)
        self.assertEqual(code, 0, out)
        self.assertNotIn("runtime error", out)
        folder.cleanup()

    def test_wrong_text_crashes(self):
        binary, folder, _, _ = build("errors/wrong_text.c", FLAGS)
        out, code = run(binary)
        self.assertNotEqual(code, 0)
        self.assertIn("SEGV", out)
        folder.cleanup()

    def test_shared_owner_is_a_double_free(self):
        binary, folder, _, _ = build("errors/shared_owner.c", FLAGS, ["payment.c"])
        out, code = run(binary)
        self.assertNotEqual(code, 0)
        self.assertIn("double-free", out)
        folder.cleanup()

    def test_ubsan_finds_the_overflow(self):
        binary, folder, _, _ = build("errors/overflow_compare.c", [*BASE, "-g", "-fsanitize=undefined"])
        self.assertIn("signed integer overflow", run(binary, "2147483647\n")[0])
        self.assertNotIn("runtime error", run(binary, "5\n")[0])
        folder.cleanup()

    def test_sanitizers_find_the_index_only_if_that_path_runs(self):
        binary, folder, _, _ = build("errors/index_out.c", FLAGS)
        self.assertIn("out of bounds", run(binary, "4\n")[0])
        self.assertEqual(run(binary, "2\n"), ("grades[2] = 17\n", 0))
        folder.cleanup()

    def test_sanitizers_do_not_find_the_uninitialized_read(self):
        binary, folder, _, _ = build("errors/uninitialized.c", FLAGS)
        out, code = run(binary, "30\n")
        self.assertEqual(code, 0, out)
        self.assertNotIn("runtime error", out)
        self.assertNotIn("AddressSanitizer", out)
        folder.cleanup()

    def test_valgrind_finds_the_uninitialized_read(self):
        binary, folder, _, _ = build("errors/uninitialized.c", [*BASE, "-g", "-O0"])
        done = subprocess.run(
            ["valgrind", str(binary)], input="30\n", text=True, encoding="utf-8", errors="replace",
            stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=60,
        )
        self.assertIn("uninitialised", done.stdout)
        folder.cleanup()

    def valgrind_output(self, source, stdin="", extra=()):
        binary, folder, _, _ = build(source, [*BASE, "-g", "-O0"], extra)
        done = subprocess.run(
            ["valgrind", str(binary)], input=stdin, text=True, encoding="utf-8", errors="replace",
            stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=60,
        )
        folder.cleanup()
        return done.stdout

    def test_valgrind_does_not_see_the_overflow(self):
        self.assertIn("ERROR SUMMARY: 0 errors", self.valgrind_output("errors/overflow_compare.c", "2147483647\n"))

    def test_valgrind_does_not_see_the_index(self):
        out = self.valgrind_output("errors/index_out.c", "4\n")
        self.assertNotIn("Invalid read", out)

    def test_valgrind_does_not_see_the_wrong_variant(self):
        self.assertIn("ERROR SUMMARY: 0 errors", self.valgrind_output("errors/wrong_variant.c"))

    def test_valgrind_finds_the_double_free(self):
        self.assertIn("Invalid free", self.valgrind_output("errors/shared_owner.c", extra=["payment.c"]))

    def test_valgrind_does_not_see_the_double_to_int(self):
        self.assertIn("ERROR SUMMARY: 0 errors", self.valgrind_output("errors/double_too_big.c", "3e10\n"))

    def test_double_to_int_needs_its_own_sanitizer(self):
        binary, folder, _, _ = build("errors/double_too_big.c", FLAGS)
        self.assertNotIn("runtime error", run(binary, "3e10\n")[0])
        folder.cleanup()
        binary, folder, _, _ = build("errors/double_too_big.c",
                                     [*BASE, "-g", "-fsanitize=undefined,float-cast-overflow"])
        self.assertIn("outside the range", run(binary, "3e10\n")[0])
        folder.cleanup()


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 value.c y table.c de esa carpeta, por separado
de las pruebas de ustedes. Corren con ASan, así que una fuga también hace fallar la prueba.
"""
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
FLAGS = ["-Wall", "-Wextra", "-std=c17", "-g", "-fsanitize=address,undefined"]


def build_and_run(main_source, sources, files=None):
    """Devuelve (salida, código de salida) de un main propio, en una carpeta temporal."""
    with tempfile.TemporaryDirectory() as name:
        folder = pathlib.Path(name)
        for file_name, content in (files or {}).items():
            (folder / file_name).write_text(content, encoding="utf-8")
        driver = folder / "driver.c"
        driver.write_text(main_source, encoding="utf-8")
        binary = folder / "driver"
        built = subprocess.run(
            ["gcc", *FLAGS, f"-I{HERE / 'lab'}", str(driver), *map(str, sources), "-o", str(binary)],
            text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
        )
        if built.returncode != 0:
            raise AssertionError(f"No compila:\n{built.stdout}")
        done = subprocess.run(
            [str(binary)], cwd=folder, text=True, encoding="utf-8", errors="replace",
            stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=30,
        )
        return done.stdout, done.returncode


def run_value(body):
    source = ('#include <stdio.h>\n#include <string.h>\n#include "value.h"\n'
              "int main(void) {\n" + body + "\nreturn 0;\n}\n")
    return build_and_run(source, [SOURCE / "value.c"])


def run_table(body, csv=None):
    source = ('#include <stdio.h>\n#include "table.h"\n'
              "int main(void) {\n" + body + "\nreturn 0;\n}\n")
    files = {"in.csv": csv} if csv is not None else None
    return build_and_run(source, [SOURCE / "value.c", SOURCE / "table.c"], files)


def summary_body(column):
    return (
        'Table *t = table_load("in.csv");\n'
        'if (t == NULL) { printf("NULL\\n"); return 0; }\n'
        f"ColumnSummary s = table_summary(t, {column});\n"
        'printf("%d %d %d %d %.2f %.2f %.2f\\n", s.integers, s.decimals, s.strings, s.nones,'
        " s.sum, s.min, s.max);\n"
        "table_free(t);"
    )


class LabCase(unittest.TestCase):
    longMessage = False

    def check(self, result, wanted, what):
        out, code = result
        if code != 0:
            self.fail(f"{what}: el programa terminó con error.\n{out}")
        if out.strip() != wanted:
            self.fail(f"{what}\n  se esperaba: {wanted}\n  se obtuvo:   {out.strip()}")


@unittest.skipUnless((SOURCE / "value.c").exists(), f"no está {LAB_DIR}/value.c")
class ValueModule(LabCase):
    def test_constructors_set_the_tag_and_the_variant(self):
        body = """
        Value a = value_none(); Value b = value_int(-3); Value c = value_double(2.5);
        Value d = value_string("Ate");
        printf("%d %d %d %d\\n", a.kind == VALUE_NONE, b.kind == VALUE_INT,
               c.kind == VALUE_DOUBLE, d.kind == VALUE_STRING);
        if (c.kind == VALUE_DOUBLE) { printf("%.1f\\n", c.as.number); }
        if (d.kind == VALUE_STRING) { printf("%s\\n", d.as.text); }
        value_free(&d);
        """
        self.check(run_value(body), "1 1 1 1\n2.5\nAte", "Los constructores no dejan la etiqueta o el dato esperado.")

    def test_string_is_a_copy(self):
        body = """
        char name[] = "Ate";
        Value v = value_string(name);
        name[0] = 'X';
        if (v.kind == VALUE_STRING) { printf("%s %d\\n", v.as.text, v.as.text != name); }
        value_free(&v);
        """
        self.check(run_value(body), "Ate 1", "value_string debe guardar una copia en memoria nueva.")

    def test_string_can_be_empty(self):
        body = """
        Value v = value_string("");
        printf("%d\\n", v.kind == VALUE_STRING);
        if (v.kind == VALUE_STRING) { printf("[%s]\\n", v.as.text); }
        value_free(&v);
        """
        self.check(run_value(body), "1\n[]", "Una cadena vacía también es una cadena.")

    def test_print_each_variant(self):
        body = """
        Value values[5] = {value_none(), value_int(-42), value_double(2.5), value_double(3.0),
                           value_string("hora punta")};
        for (int i = 0; i < 5; i++) { value_print(&values[i]); printf(";"); }
        for (int i = 0; i < 5; i++) { value_free(&values[i]); }
        """
        self.check(run_value(body), '(nada);-42;2.50;3.00;"hora punta";', "value_print no escribe lo que dice value.h.")

    def test_print_does_not_add_a_newline(self):
        body = 'Value v = value_int(1); value_print(&v); value_print(&v); value_free(&v);'
        self.check(run_value(body), "11", "value_print no debe escribir un salto de línea.")

    def test_equal(self):
        body = """
        Value none = value_none(), none2 = value_none();
        Value i = value_int(3), i2 = value_int(3), i3 = value_int(4);
        Value d = value_double(3.0), d2 = value_double(3.0), d3 = value_double(3.5);
        Value s = value_string("Ate"), s2 = value_string("Ate"), s3 = value_string("Lima");
        printf("%d%d%d", value_equal(&none, &none2), value_equal(&i, &i2), value_equal(&d, &d2));
        printf("%d ", value_equal(&s, &s2));
        printf("%d%d%d", value_equal(&i, &i3), value_equal(&d, &d3), value_equal(&s, &s3));
        printf("%d%d%d", value_equal(&i, &d), value_equal(&none, &i), value_equal(&s, &none));
        value_free(&s); value_free(&s2); value_free(&s3);
        """
        self.check(run_value(body), "1111 000000", "value_equal no cumple lo que dice value.h.")

    def test_equal_does_not_read_another_variant(self):
        # Los bytes del 0 y del 0.0 coinciden en lo que se compara: solo la etiqueta los separa.
        body = """
        Value a = value_int(0); Value b = value_double(0.0);
        printf("%d\\n", value_equal(&a, &b));
        """
        self.check(run_value(body), "0", "value_equal debe mirar primero la etiqueta.")

    def test_free_leaves_nothing_and_can_be_repeated(self):
        body = """
        Value v = value_string("Ate");
        value_free(&v);
        printf("%d\\n", v.kind == VALUE_NONE);
        value_free(&v);
        Value n = value_int(3);
        value_free(&n);
        printf("%d\\n", n.kind == VALUE_NONE);
        """
        self.check(run_value(body), "1\n1", "value_free debe liberar y dejar el valor como nada.")

    def test_a_string_that_is_not_freed_is_reported(self):
        # Comprueba que aquí sí se detectan las fugas: si no, las otras pruebas no dirían nada.
        out, code = build_and_run(
            '#include <stdlib.h>\nint main(void) { char *p = malloc(8); p = NULL; return p != NULL; }\n', [])
        self.assertNotEqual(code, 0, "Este entorno no detecta fugas, y estas pruebas no sirven aquí.")
        self.assertIn("LeakSanitizer", out)

    def test_parse(self):
        cases = {
            "": "(nada)", "42": "42", "-7": "-7", "0": "0", "2.50": "2.50", "-4.5": "-4.50",
            "Callao": '"Callao"', "hora punta": '"hora punta"', "-": '"-"', "3x": '"3x"',
            ".5": '".5"', "5.": '"5."', "1.2.3": '"1.2.3"', "1e5": '"1e5"', "nan": '"nan"',
            "inf": '"inf"', "2147483647": "2147483647", "2147483648": '"2147483648"',
            "99999999999999999999": '"99999999999999999999"',
            "-2147483647": "-2147483647", "-2147483648": '"-2147483648"',
            "1" * 70 + ".5": '"' + "1" * 70 + '.5"',
        }
        for field, wanted in cases.items():
            with self.subTest(field=field):
                body = f'Value v = value_parse("{field}"); value_print(&v); value_free(&v);'
                self.check(run_value(body), wanted, f"value_parse con el campo [{field}]")


@unittest.skipUnless((SOURCE / "table.c").exists(), f"no está {LAB_DIR}/table.c")
class TableModule(LabCase):
    CSV = "a,b,c\n1,2.5,x\n,-3,y\n4,,\n"

    def test_size_and_names(self):
        body = """
        Table *t = table_load("in.csv");
        if (t == NULL) { printf("NULL\\n"); return 0; }
        printf("%d %d %s %s\\n", table_rows(t), table_columns(t), table_column_name(t, 0),
               table_column_name(t, 2));
        table_free(t);
        """
        self.check(run_table(body, self.CSV), "3 3 a c", "table_load debe leer todas las filas.")

    def test_cells_have_the_right_variant(self):
        body = """
        Table *t = table_load("in.csv");
        if (t == NULL) { printf("NULL\\n"); return 0; }
        for (int r = 0; r < table_rows(t); r++) {
            for (int c = 0; c < table_columns(t); c++) {
                value_print(table_cell(t, r, c));
                printf(" ");
            }
        }
        table_free(t);
        """
        self.check(run_table(body, self.CSV), '1 2.50 "x" (nada) -3 "y" 4 (nada) (nada)',
                   "Las celdas no tienen lo que dice el archivo.")

    def test_only_the_header_gives_an_empty_table(self):
        body = """
        Table *t = table_load("in.csv");
        if (t == NULL) { printf("NULL\\n"); return 0; }
        printf("%d %d\\n", table_rows(t), table_columns(t));
        table_free(t);
        """
        self.check(run_table(body, "a,b\n"), "0 2", "Una tabla sin filas también es válida.")

    def test_many_rows_need_to_grow_and_do_not_leak(self):
        csv = "name,n\n" + "".join(f"fila{i},{i}\n" for i in range(2000))
        body = """
        Table *t = table_load("in.csv");
        if (t == NULL) { printf("NULL\\n"); return 0; }
        printf("%d ", table_rows(t));
        value_print(table_cell(t, 1999, 0));
        table_free(t);
        """
        self.check(run_table(body, csv), '2000 "fila1999"', "Con 2000 filas algo falla.")

    def test_bad_files_give_null_without_leaks(self):
        bad = {
            "falta un campo": "a,b\n1,2\n3\n",
            "sobra un campo": "a,b\n1,2\n3,4,5\n",
            "falla tras cadenas": "a,b\nx,y\nz,w\n1\n",
            "vacío": "",
        }
        body = 'Table *t = table_load("in.csv"); printf("%d\\n", t == NULL); table_free(t);'
        for why, csv in bad.items():
            with self.subTest(why=why):
                self.check(run_table(body, csv), "1", f"table_load debe devolver NULL: {why}.")

    def test_missing_file_and_free_of_null(self):
        body = 'Table *t = table_load("nada.csv"); printf("%d\\n", t == NULL); table_free(NULL);'
        self.check(run_table(body), "1", "Un archivo que no existe da NULL, y table_free acepta NULL.")

    def test_summary_counts_each_variant(self):
        csv = "c\n1\n2.5\nx\n\n3\n"
        # La línea vacía se salta: en una tabla de una columna no hay forma de escribir nada.
        self.check(run_table(summary_body(0), csv), "2 1 1 0 6.50 1.00 3.00",
                   "El resumen no cuenta bien, o no suma enteros con decimales.")

    def test_summary_with_nothing_in_a_cell(self):
        self.check(run_table(summary_body(1), "a,b\n1,\n2,5\n"), "1 0 0 1 5.00 5.00 5.00",
                   "Una celda con nada se cuenta, y no entra en el mínimo.")

    def test_summary_min_and_max_with_negatives(self):
        # Si el mínimo o el máximo empiezan en 0, estas dos columnas lo descubren.
        self.check(run_table(summary_body(0), "a,b\n-4.5,3\n-1,7\n"), "1 1 0 0 -5.50 -4.50 -1.00",
                   "El máximo de números negativos no puede ser 0.")
        self.check(run_table(summary_body(1), "a,b\n-4.5,3\n-1,7\n"), "2 0 0 0 10.00 3.00 7.00",
                   "El mínimo de números positivos no puede ser 0.")

    def test_summary_without_numbers_gives_zero(self):
        self.check(run_table(summary_body(0), "a\nx\ny\n"), "0 0 2 0 0.00 0.00 0.00",
                   "Sin números, la suma, el mínimo y el máximo valen 0.")

    def test_report_on_the_data_file(self):
        with tempfile.TemporaryDirectory() as name:
            binary = pathlib.Path(name) / "report"
            built = subprocess.run(
                ["gcc", *FLAGS, f"-I{HERE / 'lab'}", str(HERE / "lab" / "report.c"),
                 str(SOURCE / "value.c"), str(SOURCE / "table.c"), "-o", str(binary)],
                text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
            )
            self.assertEqual(built.returncode, 0, built.stdout)
            done = subprocess.run(
                [str(binary), str(HERE / "lab" / "data" / "viajes.csv")], text=True, encoding="utf-8",
                stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=30,
            )
        if done.returncode != 0:
            self.fail(f"report terminó con error:\n{done.stdout}")
        lines = [re.sub(r"\s+", " ", line).strip() for line in done.stdout.splitlines()]
        for wanted in ("7 filas y 4 columnas", "pasajeros 5 0 1 1 105.00 21.00 9.00 31.00",
                       "tarifa 2 5 0 0 17.80 2.54 1.80 3.50", '"Ate" | (nada) | 1.80 | "sin conteo"'):
            with self.subTest(line=wanted):
                self.assertTrue(wanted in lines, f"report no escribió esta línea: {wanted}")


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

    def test_each_function_has_tests(self):
        text = re.sub(r"//[^\n]*", "", (SOURCE / "value_tests.c").read_text(encoding="utf-8"))
        for name in ("value_double", "value_string", "value_equal", "value_free"):
            with self.subTest(function=name):
                self.assertRegex(text, rf"\b{name}\(", f"value_tests.c no prueba {name}")

    def test_your_tests_pass(self):
        source = (SOURCE / "value_tests.c").read_text(encoding="utf-8")
        out, code = build_and_run(source, [SOURCE / "value.c"])
        self.assertEqual(code, 0, 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 os
import platform
import pathlib
import re
import select
import subprocess
import unittest

try:
    import pty
except ImportError:
    pty = None

HERE = pathlib.Path(__file__).parent
ROOT = HERE.parent.parent
NOTES = ROOT / "notes" / "week-07.md"
SLIDES = ROOT / "slides" / "week-07.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 una sesión de gdb. Las direcciones y los números de proceso no se comparan.
GDB_SESSION = "<!-- sesión de gdb -->"
# Antes de lo que escribe report con la solución.
REPORT_OUTPUT = "<!-- salida de report -->"
# Las sesiones se capturaron en esta arquitectura: en otra pueden cambiar algunas líneas.
GDB_MACHINE = "aarch64"
GDB_FLAGS = ["-Wall", "-Wextra", "-std=c17", "-g", "-O0"]
GDB_SOURCES = {"payment_demo": ["payment_demo.c", "payment.c"]}


def normalize(text):
    # Según el idioma del entorno, gcc usa comillas tipográficas o rectas.
    return text.replace("‘", "'").replace("’", "'")


def run_shell(command):
    done = subprocess.run(
        ["bash", "-c", command], cwd=HERE, text=True, encoding="utf-8", errors="replace",
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=60,
    )
    return normalize(done.stdout)


def clean_lines(text):
    return [line.rstrip() for line in text.splitlines()]


def split_commands(lines):
    """Parte un bloque de terminal en pares (comando, líneas de salida)."""
    pairs = []
    for line in lines:
        if line.startswith("$ "):
            pairs.append((line[2:], []))
        elif pairs:
            pairs[-1][1].append(normalize(line.rstrip()))
    return pairs


def note_blocks(language):
    """Devuelve (líneas, marca) de cada bloque de los apuntes."""
    blocks, current, mark, previous = [], None, "", ""
    for raw in NOTES.read_text(encoding="utf-8").splitlines():
        line = re.sub(r"^> ?", "", raw)
        if current is None:
            if line.strip() == "```" + language:
                current, mark = [], previous.strip()
            elif line.strip():
                previous = line
        elif line.strip() == "```":
            blocks.append((current, mark))
            current = None
        else:
            current.append(line)
    return blocks


def slide_blocks(environments):
    text = SLIDES.read_text(encoding="utf-8")
    pattern = r"\\begin\{(?:%s)\}[^\n]*\n(.*?)\\end\{(?:%s)\}" % (environments, environments)
    return [match.group(1).splitlines() for match in re.finditer(pattern, text, re.S)]


def known_code_lines():
    lines = set()
    sources = list(HERE.glob("*.c")) + list(HERE.glob("*.h")) + list((HERE / "errors").glob("*.c"))
    sources += list((HERE / "lab").glob("*.c")) + list((HERE / "lab").glob("*.h"))
    for source in sources:
        lines.update(line.strip() for line in source.read_text(encoding="utf-8").splitlines())
    return lines


def stable_report_line(line):
    line = normalize(line).rstrip()
    line = line.replace(str(HERE.resolve()) + "/", "")
    line = re.sub(r"0x[0-9a-fA-F]+", "ADDR", line)
    line = re.sub(r"==\d+==", "==N==", line)
    return line.strip()


def assert_in_order(case, wanted, got, label):
    position = 0
    for line in wanted:
        if not line.strip() or line.strip() == "...":
            continue
        with case.subTest(label=label, line=line):
            found = [i for i in range(position, len(got)) if line in got[i]]
            case.assertTrue(found, f"no aparece en la salida real: {line}")
            if found:
                position = found[0] + 1


def real_gdb_session(program, commands):
    """Corre gdb en una terminal de verdad: cada comando espera al prompt anterior."""
    os.environ["TERM"] = "dumb"
    pid, fd = pty.fork()
    if pid == 0:
        os.execvp("gdb", ["gdb", "-q", program])

    def read_until_prompt():
        data = b""
        while not data.endswith(b"(gdb) "):
            ready, _, _ = select.select([fd], [], [], 10)
            if not ready:
                break
            try:
                chunk = os.read(fd, 4096)
            except OSError:
                break
            if not chunk:
                break
            data += chunk
        return data

    output = read_until_prompt()
    for command in commands:
        os.write(fd, (command + "\n").encode())
        output += read_until_prompt()
    os.close(fd)
    os.waitpid(pid, 0)
    return output.decode("utf-8", errors="replace").replace("\r", "")


def stable_gdb_lines(lines):
    """Quita lo que cambia entre corridas y deja el resto, sin las direcciones."""
    stable = []
    for line in lines:
        line = line.rstrip()
        if not line or line.startswith(("Starting program", "(gdb)", "[Thread", "Using host")):
            continue
        line = re.sub(r"0x[0-9a-f]+", "ADDR", line)
        line = re.sub(r"process \d+", "process N", line)
        # Lo que queda en las otras variantes de una union depende de la memoria.
        line = re.sub(r"cash = [^,]+, card_end = -?\d+", "cash = X, card_end = Y", line)
        stable.append(line)
    return stable


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 in (GDB_SESSION, REPORT_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(), "faltan los apuntes")
    @unittest.skipUnless(pty and platform.machine() == GDB_MACHINE, "las sesiones son de aarch64")
    def test_gdb_sessions_in_notes(self):
        probe = subprocess.run(
            ["gdb", "-q", "-batch", "-ex", "run", "/bin/true"], text=True,
            stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
        )
        if "exited normally" not in probe.stdout:
            self.skipTest("gdb no puede correr programas aquí")
        checked = 0
        for lines, mark in note_blocks("text"):
            if mark != GDB_SESSION:
                continue
            program = lines[0].split()[-1]
            name = pathlib.Path(program).name.removesuffix("_gdb")
            sources = GDB_SOURCES.get(name, [name + ".c"])
            subprocess.run(["gcc", *GDB_FLAGS, *sources, "-o", program], cwd=HERE, check=True)
            commands = [line[6:].strip() for line in lines if line.startswith("(gdb) ")]
            cwd = os.getcwd()
            os.chdir(HERE)
            try:
                actual = real_gdb_session(program, commands)
            finally:
                os.chdir(cwd)
            wanted = stable_gdb_lines(lines[1:])
            got = stable_gdb_lines(actual.splitlines())
            position = 0
            for line in wanted:
                with self.subTest(program=program, line=line):
                    found = [i for i in range(position, len(got)) if got[i] == line]
                    self.assertTrue(found, f"no aparece en la sesión real: {line}")
                    if found:
                        position = found[0] + 1
            checked += 1
        self.assertGreater(checked, 1)

    @unittest.skipUnless(NOTES.exists() and (HERE / "solution").exists(), "falta la solución")
    def test_report_output_in_notes(self):
        checked = 0
        for lines, mark in note_blocks("text"):
            if mark == REPORT_OUTPUT:
                self.assertEqual(lines[0], "$ build/lab/report lab/data/viajes.csv")
                actual = clean_lines(run_shell("build/solution/report lab/data/viajes.csv"))
                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):
        # Las sesiones de gdb y los informes van en otro entorno, sesiongdb, que no se compara.
        for block in slide_blocks("terminal|salida|salidalarga"):
            for command, expected in split_commands(block):
                actual = clean_lines(run_shell(command))
                position = 0
                # Una diapositiva puede omitir líneas, pero no cambiarlas ni reordenarlas.
                for line in expected:
                    line = line.strip()
                    if not line:
                        continue
                    with self.subTest(command=command, line=line):
                        found = [i for i in range(position, len(actual)) if actual[i].strip() == line]
                        self.assertTrue(found, f"no aparece en la salida real: {line}")
                        if found:
                            position = found[0] + 1

    @unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
    def test_code_blocks_in_slides_exist(self):
        known = known_code_lines()
        for block in slide_blocks("codigo|codigochico|codigomini"):
            for line in block:
                if line.strip() and line.strip() != "...":
                    with self.subTest(line=line):
                        self.assertIn(line.strip(), known)


if __name__ == "__main__":
    unittest.main()

Laboratorio

Archivos de partida del laboratorio de la semana.

lab/README.mdDescargar lab/README.md
# Laboratorio de la semana 7: un valor con varios tipos

Esta semana hay una sola sesión de laboratorio, porque la otra es para la Práctica 2. Está
pensada para unos 90 minutos de trabajo. Los comandos se corren desde `code/week-07`. Las
ideas están en los apuntes, en la sección "El laboratorio, paso a paso".

| Archivo | Qué es | ¿Lo cambian? |
|---|---|---|
| `value.h` | La interfaz de `Value`: lo que promete cada función | No |
| `value.c` | `value_none`, `value_int` y `value_parse` hechos, lo demás por escribir | **Sí** |
| `value_tests.c` | Sus pruebas con `assert` | **Sí** |
| `table.h` | La interfaz de la tabla, con un tipo opaco | No |
| `table.c` | La lectura del archivo hecha; `add_row`, `table_free` y `table_summary` por escribir | **Sí** |
| `report.c` | El programa que escribe las filas y el resumen | No |
| `data/` | `viajes.csv` y `alturas.csv`, para probar | No |

```sh
make lab                  # compila value_tests y report en build/lab/
build/lab/value_tests     # corre sus pruebas
build/lab/report lab/data/viajes.csv
make test-lab             # comprueba todo el laboratorio; falla hasta que terminen
```

Todo se compila con ASan: si una cadena no se libera, el programa termina con un informe de
LeakSanitizer y un código de error, y la prueba falla.

## Parte 1: leer (unos 15 minutos)

Lean `value.h` completo, luego la parte hecha de `value.c`, y luego `table.h`. Respondan por
escrito, sin ejecutar nada.

1. ¿Por qué `value_none` no le da ningún valor a `v.as`? ¿Qué parte del programa podría notarlo?
2. En `value_parse`, ¿qué devuelve el campo `2147483648`? ¿En qué línea se decide, y por qué esa
   revisión va antes de multiplicar?
3. ¿Por qué `Value` deja ver sus campos, y `Table` no?
4. `table_cell` devuelve un `const Value *`. ¿Quién libera esa celda? ¿Qué dice el `const`?

## Parte 2: el módulo `Value` (unos 35 minutos)

Escriban en `value.c`, en este orden, y corran sus pruebas después de cada una:

1. `value_double`, igual que `value_int`.
2. `value_string` y `value_free`. Aquí está la memoria: `value_string` copia, y `value_free` libera
   solo si la variante es una cadena.
3. `value_print` y `value_equal`, cada una con un `switch` sobre la etiqueta y **sin `default`**, para
   que `-Wall` les avise si falta un caso.

Agreguen a `value_tests.c` pruebas de `value_double`, `value_string`, `value_equal` y `value_free`,
con `test_value_int` como modelo. Toda cadena que cree una prueba se libera en esa prueba.

## Parte 3: la tabla y el resumen (unos 40 minutos)

Lean primero `table_load` y `read_rows` en `table.c`. El `main` de `report.c` usa `argv`, y los apuntes explican por qué. Luego escriban:

1. `add_row`: parte la línea con `split_line`, revisa el número de campos, asegura espacio con
   `reserve_row` y guarda en cada celda lo que devuelve `value_parse`. Son pocas líneas.
2. `table_free`: cada celda posee su cadena, así que primero las celdas, luego el arreglo y al final la
   tabla. Debe aceptar `NULL`.
3. `table_summary`: un `switch` por celda. Decidan cómo fijar el mínimo y el máximo: una columna con
   solo números negativos no tiene máximo 0, y una con solo positivos no tiene mínimo 0.

Con eso, esto debe coincidir con los apuntes:

```sh
build/lab/report lab/data/viajes.csv
```

Corran también `build/lab/report lab/data/alturas.csv`, calculen a mano la fila de `minima`, y al final
`make test-lab`.

**Si no alcanza el tiempo:** lo que no terminen lo terminan antes de la próxima clase. El orden de arriba
deja lo más importante primero: `Value`, y después la tabla.

## Si sobra tiempo

1. Escriban `Value value_copy(const Value *v)`, que devuelva un valor independiente. ¿Qué variante necesita
   más que una asignación?
2. Agreguen una variante `VALUE_BOOL`, para los campos `true` y `false`. Compilen antes de tocar los `switch`
   y lean lo que dice `gcc`: esa lista es la de los lugares por cambiar.
lab/report.cDescargar lab/report.c
// Lee un archivo CSV y escribe sus filas y un resumen de cada columna.
#include <stdio.h>

#include "table.h"

static void print_rows(const Table *t) {
    for (int r = 0; r < table_rows(t); r++) {
        for (int c = 0; c < table_columns(t); c++) {
            if (c > 0) {
                printf(" | ");
            }
            value_print(table_cell(t, r, c));
        }
        printf("\n");
    }
}

static void print_summary(const Table *t, int column) {
    ColumnSummary s = table_summary(t, column);
    int numbers = s.integers + s.decimals;

    printf("%-12s %7d %9d %7d %6d", table_column_name(t, column), s.integers, s.decimals,
           s.strings, s.nones);
    if (numbers > 0) {
        printf(" %9.2f %7.2f %7.2f %7.2f", s.sum, s.sum / numbers, s.min, s.max);
    }
    printf("\n");
}

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "Uso: %s archivo.csv\n", argv[0]);
        return 1;
    }

    Table *t = table_load(argv[1]);
    if (t == NULL) {
        fprintf(stderr, "No se pudo leer %s\n", argv[1]);
        return 1;
    }

    printf("%d filas y %d columnas\n\n", table_rows(t), table_columns(t));
    print_rows(t);

    printf("\n%-12s %7s %9s %7s %6s %9s %7s %7s %7s\n", "columna", "enteros", "decimales",
           "cadenas", "nada", "suma", "media", "min", "max");
    for (int c = 0; c < table_columns(t); c++) {
        print_summary(t, c);
    }

    table_free(t);
    return 0;
}
lab/table.cDescargar lab/table.c
#include "table.h"

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

#define LINE_SIZE 256
#define NAME_SIZE 32
#define FIRST_CAPACITY 4

struct table {
    int columns;
    char names[TABLE_MAX_COLUMNS][NAME_SIZE];
    int rows;
    int capacity;  // cuántas filas caben en cells
    Value *cells;  // rows * columns celdas, fila por fila; la tabla es owner de ellas
};

// --- Hecho: lean estas funciones, no hace falta cambiarlas. ---

// Parte la línea en campos, separados por comas. Cambia cada coma por un '\0', y deja en
// fields[i] la dirección de cada campo. Un campo puede quedar vacío.
// Devuelve cuántos campos hay, o -1 si son más que TABLE_MAX_COLUMNS.
static int split_line(char *line, char *fields[]) {
    int count = 0;
    fields[count] = line;
    count++;
    for (int i = 0; line[i] != '\0'; i++) {
        if (line[i] == ',') {
            if (count == TABLE_MAX_COLUMNS) {
                return -1;
            }
            line[i] = '\0';
            fields[count] = &line[i + 1];
            count++;
        }
    }
    return count;
}

// Quita el salto de línea final, con o sin '\r'. Devuelve 0 si la línea no entró completa.
static int strip_newline(char *line, FILE *file) {
    size_t length = strlen(line);
    if (length > 0 && line[length - 1] == '\n') {
        line[length - 1] = '\0';
        length--;
    } else if (length + 1 == LINE_SIZE && !feof(file)) {
        return 0;
    }
    if (length > 0 && line[length - 1] == '\r') {
        line[length - 1] = '\0';
    }
    return 1;
}

// Asegura que quepa una fila más. Devuelve 1 si pudo, y 0 si no hay memoria.
static int reserve_row(Table *t) {
    if (t->rows < t->capacity) {
        return 1;
    }
    int capacity = t->capacity == 0 ? FIRST_CAPACITY : t->capacity * 2;
    size_t bytes = (size_t) capacity * (size_t) t->columns * sizeof(Value);
    Value *cells = realloc(t->cells, bytes);
    if (cells == NULL) {
        return 0;
    }
    t->cells = cells;
    t->capacity = capacity;
    return 1;
}

static int read_header(Table *t, char *line) {
    char *fields[TABLE_MAX_COLUMNS];
    int count = split_line(line, fields);
    if (count < 0 || line[0] == '\0') {
        return 0;
    }
    for (int c = 0; c < count; c++) {
        if (strlen(fields[c]) >= NAME_SIZE) {
            return 0;
        }
        strcpy(t->names[c], fields[c]);
    }
    t->columns = count;
    return 1;
}

static int add_row(Table *t, char *line);

// Lee el resto del archivo, fila por fila. Se salta las líneas vacías.
static int read_rows(Table *t, FILE *file) {
    char line[LINE_SIZE];
    while (fgets(line, LINE_SIZE, file) != NULL) {
        if (!strip_newline(line, file)) {
            return 0;
        }
        if (line[0] != '\0' && !add_row(t, line)) {
            return 0;
        }
    }
    return 1;
}

static int read_table(Table *t, FILE *file) {
    char line[LINE_SIZE];
    if (fgets(line, LINE_SIZE, file) == NULL) {
        return 0;
    }
    return strip_newline(line, file) && read_header(t, line) && read_rows(t, file);
}

Table *table_load(const char *name) {
    FILE *file = fopen(name, "r");
    if (file == NULL) {
        return NULL;
    }

    Table *t = malloc(sizeof(Table));
    if (t == NULL) {
        fclose(file);
        return NULL;
    }
    t->columns = 0;
    t->rows = 0;
    t->capacity = 0;
    t->cells = NULL;

    int ok = read_table(t, file);
    fclose(file);
    if (!ok) {
        table_free(t);
        return NULL;
    }
    return t;
}

int table_rows(const Table *t) {
    return t->rows;
}

int table_columns(const Table *t) {
    return t->columns;
}

const char *table_column_name(const Table *t, int column) {
    return t->names[column];
}

const Value *table_cell(const Table *t, int row, int column) {
    return &t->cells[row * t->columns + column];
}

// --- Por escribir. ---

// Convierte una línea en una fila nueva. Devuelve 1 si pudo, y 0 si la fila no tiene tantos
// campos como columnas o si no hay memoria. Si devuelve 0, rows no cambió y no quedó nada a
// medias. Usen split_line, reserve_row y value_parse.
static int add_row(Table *t, char *line) {
    (void) t;
    (void) line;
    (void) split_line;
    (void) reserve_row;
    return 0;
}

void table_free(Table *t) {
    (void) t;
}

ColumnSummary table_summary(const Table *t, int column) {
    (void) t;
    (void) column;
    ColumnSummary summary = {0, 0, 0, 0, 0.0, 0.0, 0.0};
    return summary;
}
lab/table.hDescargar lab/table.h
// Una tabla leída de un archivo CSV: la primera línea tiene los nombres de las columnas,
// y cada celda es un Value. El tipo es opaco: los campos solo los conoce table.c.
#ifndef TABLE_H
#define TABLE_H

#include "value.h"

#define TABLE_MAX_COLUMNS 8

typedef struct table Table;

// Lee el archivo y devuelve la tabla, o NULL si no se puede abrir, si no tiene encabezado,
// si alguna fila no tiene tantos campos como columnas o si no hay memoria.
// Quien llama es owner de la tabla y la libera con table_free.
Table *table_load(const char *name);

// Libera la tabla y todo lo que posee, también las cadenas de sus celdas. Acepta NULL.
void table_free(Table *t);

int table_rows(const Table *t);
int table_columns(const Table *t);
const char *table_column_name(const Table *t, int column);

// Devuelve la celda, sin copiarla. La sigue poseyendo la tabla: quien llama no la libera.
const Value *table_cell(const Table *t, int row, int column);

// Lo que resume una columna. La suma, el mínimo y el máximo miran los enteros y los decimales.
// Si la columna no tiene números, sum, min y max valen 0.
typedef struct {
    int integers;
    int decimals;
    int strings;
    int nones;
    double sum;
    double min;
    double max;
} ColumnSummary;

ColumnSummary table_summary(const Table *t, int column);

#endif
lab/value_tests.cDescargar lab/value_tests.c
// Pruebas del módulo Value con assert.
// Agreguen al menos una función de prueba para value_double, value_string, value_equal y
// value_free, y llámenla desde main. Un valor con cadena también se libera en las pruebas.
#include <assert.h>
#include <stdio.h>

#include "value.h"

static void test_value_int(void) {
    Value v = value_int(42);

    assert(v.kind == VALUE_INT);
    assert(v.as.integer == 42);
    value_free(&v);
}

int main(void) {
    test_value_int();

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

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

// Un decimal con más caracteres queda como cadena: así, siempre cabe en un double.
#define DECIMAL_MAX_LENGTH 64

// --- Hecho: lean estas funciones, no hace falta cambiarlas. ---

Value value_none(void) {
    Value v;
    v.kind = VALUE_NONE;
    return v;
}

Value value_int(int n) {
    Value v;
    v.kind = VALUE_INT;
    v.as.integer = n;
    return v;
}

// Avanza por los dígitos desde text[i], y devuelve dónde se detuvo.
static int skip_digits(const char *text, int i) {
    while (text[i] >= '0' && text[i] <= '9') {
        i++;
    }
    return i;
}

// Convierte solo dígitos. Revisa ANTES de multiplicar: después ya sería overflow.
static int digits_to_int(const char *digits, int *result) {
    int value = 0;
    for (int i = 0; digits[i] != '\0'; i++) {
        int digit = digits[i] - '0';
        if (value > (INT_MAX - digit) / 10) {
            return 0;
        }
        value = value * 10 + digit;
    }
    *result = value;
    return 1;
}

Value value_parse(const char *field) {
    if (field[0] == '\0') {
        return value_none();
    }

    int start = field[0] == '-' ? 1 : 0;
    int end = skip_digits(field, start);

    if (end > start && field[end] == '\0') {
        int n;
        if (digits_to_int(field + start, &n)) {
            return value_int(start == 1 ? -n : n);
        }
        return value_string(field);
    }

    if (end > start && field[end] == '.') {
        int after = end + 1;
        int last = skip_digits(field, after);
        double x;
        if (last > after && field[last] == '\0' && last <= DECIMAL_MAX_LENGTH &&
            sscanf(field, "%lf", &x) == 1) {
            return value_double(x);
        }
    }
    return value_string(field);
}

// --- Por escribir. Lo que debe hacer cada una está en value.h. ---

Value value_double(double x) {
    (void) x;
    return value_none();
}

Value value_string(const char *s) {
    (void) s;
    return value_none();
}

void value_print(const Value *v) {
    (void) v;
}

int value_equal(const Value *a, const Value *b) {
    (void) a;
    (void) b;
    return 0;
}

void value_free(Value *v) {
    (void) v;
}
lab/value.hDescargar lab/value.h
// Un valor que puede ser un entero, un decimal, una cadena o nada.
// La etiqueta kind dice qué variante de la union as está activa: lean solo esa.
//
// Quien usa el módulo sí ve los campos, porque hace switch sobre kind. Por eso Value no es
// un tipo opaco. La regla es de ustedes: creen un Value solo con estas funciones.
//
// Ownership: un Value de tipo VALUE_STRING posee su cadena. Asignar un Value a otro NO copia la
// cadena, solo su dirección: el owner pasa a ser la variable nueva, y la anterior ya no se
// usa ni se libera. Todo Value se libera una vez, con value_free.
#ifndef VALUE_H
#define VALUE_H

typedef enum { VALUE_NONE, VALUE_INT, VALUE_DOUBLE, VALUE_STRING } ValueKind;

typedef struct {
    ValueKind kind;
    union {
        int integer;   // VALUE_INT
        double number; // VALUE_DOUBLE
        char *text;    // VALUE_STRING: cadena en el heap, que posee este Value
    } as;
} Value;

// Devuelve un Value sin dato.
Value value_none(void);

// Devuelve un Value con ese entero.
Value value_int(int n);

// Devuelve un Value con ese decimal.
Value value_double(double x);

// Devuelve un Value con una COPIA de s, en memoria nueva: s puede cambiar o desaparecer
// después. s no puede ser NULL. Si no hay memoria, devuelve value_none().
Value value_string(const char *s);

// Convierte un campo de un archivo, según su forma:
//   vacío -> nada
//   solo dígitos, con un - inicial opcional -> entero, si está entre -2147483647 y 2147483647
//   dígitos, un punto y dígitos, con un - opcional, hasta 64 caracteres -> decimal
//   cualquier otra cosa -> cadena
Value value_parse(const char *field);

// Escribe el valor en la salida, sin salto de línea:
//   nada: (nada)     entero: 42     decimal: con dos decimales, 2.50     cadena: "Callao", con comillas
void value_print(const Value *v);

// Devuelve 1 si son la misma variante con el mismo contenido, y 0 si no.
// Dos nada son iguales. Un entero nunca es igual a un decimal: 3 y 3.00 son distintos.
// Las cadenas se comparan por su contenido, no por su dirección.
int value_equal(const Value *a, const Value *b);

// Libera lo que el valor posee y lo deja como nada. Sirve para cualquier variante.
void value_free(Value *v);

#endif
lab/data/alturas.csvDescargar lab/data/alturas.csv
estacion,altura,minima,estado
Puno,3827,-4.5,activa
Cusco,3399,-1,activa
Lima,154,14.25,
Arequipa,2335,6,en revisión
Ticlio,4818,,sin sensor
lab/data/viajes.csvDescargar lab/data/viajes.csv
ruta,pasajeros,tarifa,nota
Chosica,18,2.50,
Callao,25,3,hora punta
Chosica,22,2.50,
Ate,,1.80,sin conteo
Callao,31,3.50,feriado
Ate,9,2,
Chosica,sin dato,2.50,lluvia

Compilación

Cómo se compilan y ejecutan los ejemplos.

MakefileDescargar Makefile
# Semana 7: tipos, conversiones y comportamiento indefinido. 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 (lab/value.c, lab/table.c y lab/value_tests.c); 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.
# Para usar gdb o valgrind se compila a mano con -g -O0 y sin sanitizers: ver los apuntes.

CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -g -fsanitize=address,undefined
BUILD = build
PROGRAMS = typedef_units trip_defines trip_enum enum_values union_bytes payment_sizes \
           sign_rule promotion narrowing

.PHONY: all lab test test-lab clean

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

$(BUILD)/%: %.c | $(BUILD)
	$(CC) $(CFLAGS) $< -o $@

$(BUILD)/payment.o: payment.c payment.h | $(BUILD)
	$(CC) $(CFLAGS) -c $< -o $@

$(BUILD)/payment_demo.o: payment_demo.c payment.h | $(BUILD)
	$(CC) $(CFLAGS) -c $< -o $@

$(BUILD)/payment_demo: $(BUILD)/payment_demo.o $(BUILD)/payment.o
	$(CC) $(CFLAGS) $^ -o $@

# Cada .c se compila por separado y luego se enlaza, como en la semana 6.
$(BUILD)/lab/%.o: lab/%.c lab/value.h lab/table.h | $(BUILD)/lab
	$(CC) $(CFLAGS) -c $< -o $@

$(BUILD)/lab/value_tests: $(BUILD)/lab/value_tests.o $(BUILD)/lab/value.o
	$(CC) $(CFLAGS) $^ -o $@

$(BUILD)/lab/report: $(BUILD)/lab/report.o $(BUILD)/lab/table.o $(BUILD)/lab/value.o
	$(CC) $(CFLAGS) $^ -o $@

$(BUILD)/solution/%.o: solution/%.c lab/value.h lab/table.h | $(BUILD)/solution
	$(CC) $(CFLAGS) -Ilab -c $< -o $@

$(BUILD)/solution/report.o: lab/report.c lab/value.h lab/table.h | $(BUILD)/solution
	$(CC) $(CFLAGS) -Ilab -c $< -o $@

$(BUILD)/solution/value_tests: $(BUILD)/solution/value_tests.o $(BUILD)/solution/value.o
	$(CC) $(CFLAGS) $^ -o $@

$(BUILD)/solution/report: $(BUILD)/solution/report.o $(BUILD)/solution/table.o $(BUILD)/solution/value.o
	$(CC) $(CFLAGS) $^ -o $@

$(BUILD) $(BUILD)/lab $(BUILD)/solution:
	mkdir -p $@

lab: $(BUILD)/lab/value_tests $(BUILD)/lab/report

# Si existe solution/, también se comprueba la solución del laboratorio.
test: all
	@if [ -d solution ]; then $(MAKE) --no-print-directory \
	  $(BUILD)/solution/value_tests $(BUILD)/solution/report; 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__