15
Computación moderna

Inteligencia artificial y ética

Computación moderna

Semana 15 — Inteligencia artificial y ética

Objetivos

  • Explicar en qué se diferencia programar reglas de entrenar un modelo.
  • Explicar por qué un modelo se evalúa con datos que no vio.
  • Explicar a grandes rasgos cómo funciona un LLM y por qué puede inventar respuestas.
  • Analizar el impacto de un sistema de software con casos reales.

Teoría — Inteligencia artificial y ética

Lectura: Apuntes, semana 15.

Temas:

  • Reglas escritas a mano frente a reglas aprendidas de los datos.
  • Aprendizaje supervisado: entrenar y evaluar. Overfitting.
  • LLMs en lo esencial: predecir el siguiente token. Por qué responden con seguridad aunque estén equivocados.
  • Por qué los fundamentos siguen importando: para juzgar una respuesta hay que saber el tema.
  • Ética: sesgo, privacidad y responsabilidad profesional, con casos reales.
  • Qué sigue en la carrera.

Laboratorio — Actividad guiada con un asistente · Repaso

Esta es la única actividad del curso donde se permite usar IA generativa.

  1. Sin asistente: en lab/tests.c, escribir pruebas que delaten los bugs de lab/suspect.c, corregirlos y comprobarlo con make test-lab.
  2. Pedirle a un asistente una función en C de las que ya escribieron en el curso.
  3. Leerla, compilarla con -Wall -Wextra -std=c17 -g -fsanitize=address,undefined y probarla con sus propias pruebas assert.
  4. Anotar qué hizo bien, qué error encontraron y cómo lo detectaron.
  5. Repasar para el examen final.

Preguntas de repaso

  1. Para detectar correo no deseado, ¿qué diferencia hay entre escribir reglas y entrenar un modelo?
  2. ¿Por qué no se evalúa un modelo con los mismos datos con los que se entrenó?
  3. ¿Por qué un LLM puede citar un artículo que no existe?
  4. Un sistema decide a quién se le aprueba un crédito. ¿Qué preguntas harían antes de confiar en él?
  5. Un asistente escribe una función que pasa la prueba que le pidieron. ¿Qué más revisarían antes de usarla?

Código de la semana

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

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

Ejemplos

Los programas que usan los apuntes y la clase.

check_suspect.cDescargar check_suspect.c
// Parte de make test-lab: llama a una función de suspect.c con los datos que recibe
// e imprime el resultado. Las respuestas esperadas están en test_lab.py.
//   prime N          is_prime(N)
//   max A B C ...    max_of_array con esos números
//   copy SIZE TEXT   copy_text en un destino de exactamente SIZE bytes
#include <stdio.h>
#include <stdlib.h>

#define MAX_ITEMS 16

int is_prime(int n);
int max_of_array(const int a[], int n);
void copy_text(char dest[], const char src[], int size);

int main(int argc, char *argv[]) {
    if (argc < 3) {
        return 2;
    }
    if (argv[1][0] == 'p') {
        printf("%d\n", is_prime(atoi(argv[2])));
    } else if (argv[1][0] == 'm') {
        int a[MAX_ITEMS];
        int n = argc - 2;
        if (n > MAX_ITEMS) {
            return 2;
        }
        for (int i = 0; i < n; i++) {
            a[i] = atoi(argv[i + 2]);
        }
        printf("%d\n", max_of_array(a, n));
    } else {
        // Solo se usan estos tamaños, cada uno con su arreglo exacto. Los bytes de partida
        // no son '\0', para que se note si la función no termina la cadena.
        int size = atoi(argv[2]);
        const char *text = argc > 3 ? argv[3] : "";
        char one[1] = {'X'};
        char two[2] = {'X', 'X'};
        char three[3] = {'X', 'X', 'X'};
        char five[5] = {'X', 'X', 'X', 'X', 'X'};
        if (size == 1) {
            copy_text(one, text, 1);
            printf("[%s]\n", one);
        } else if (size == 2) {
            copy_text(two, text, 2);
            printf("[%s]\n", two);
        } else if (size == 3) {
            copy_text(three, text, 3);
            printf("[%s]\n", three);
        } else if (size == 5) {
            copy_text(five, text, 5);
            printf("[%s]\n", five);
        } else {
            return 2;
        }
    }
    return 0;
}
corpus.txtDescargar corpus.txt
la capital de perú es lima .
la capital de chile es santiago .
la capital de colombia es bogotá .
la ciudad más poblada de perú es lima .
garcía publicó un artículo sobre redes en 2019 .
flores publicó un artículo sobre redes en 2019 .
quispe publicó un artículo sobre memoria en 2021 .
network.cDescargar network.c
// Tres neuronas en dos capas, con pesos escritos a mano, calculan XOR.
// Una sola neurona no puede: el programa prueba todos los pesos enteros de -3 a 3.
#include <stdio.h>

#define LIMIT 3

struct neuron {
    int w_a;
    int w_b;
    int base;
};

int fire(struct neuron n, int a, int b) {
    return n.w_a * a + n.w_b * b + n.base > 0;
}

// Devuelve 1 si la neurona da XOR en los cuatro casos.
int computes_xor(struct neuron n) {
    for (int a = 0; a <= 1; a++) {
        for (int b = 0; b <= 1; b++) {
            if (fire(n, a, b) != (a != b)) {
                return 0;
            }
        }
    }
    return 1;
}

int main(void) {
    struct neuron n_or = {1, 1, 0};
    struct neuron n_nand = {-1, -1, 2};
    struct neuron n_and = {1, 1, -1};

    printf("a b  or nand  salida\n");
    for (int a = 0; a <= 1; a++) {
        for (int b = 0; b <= 1; b++) {
            int first = fire(n_or, a, b);
            int second = fire(n_nand, a, b);
            printf("%d %d  %2d %4d  %6d\n", a, b, first, second, fire(n_and, first, second));
        }
    }

    int tried = 0;
    int found = 0;
    for (int w_a = -LIMIT; w_a <= LIMIT; w_a++) {
        for (int w_b = -LIMIT; w_b <= LIMIT; w_b++) {
            for (int base = -LIMIT; base <= LIMIT; base++) {
                struct neuron n = {w_a, w_b, base};
                tried++;
                found = found + computes_xor(n);
            }
        }
    }
    printf("Una sola neurona: %d de %d combinaciones de pesos dan XOR\n", found, tried);
    return 0;
}
neuron.cDescargar neuron.c
// Una neurona artificial con dos entradas. Sus pesos se ajustan con los ejemplos:
// cada error mueve los pesos un poco hacia la respuesta correcta.
// Usa enteros para poder seguirla a mano.
#include <stdio.h>

#define MAILS 4
#define MAX_ROUNDS 10

struct example {
    int words;   // palabras sospechosas
    int links;   // enlaces
    int spam;    // la etiqueta
};

struct neuron {
    int w_words;
    int w_links;
    int base;
};

int score(struct neuron n, int words, int links) {
    return n.w_words * words + n.w_links * links + n.base;
}

int predict(struct neuron n, int words, int links) {
    return score(n, words, links) > 0;
}

int main(void) {
    struct example examples[MAILS] = {{3, 2, 1}, {1, 0, 0}, {2, 2, 1}, {0, 1, 0}};
    struct neuron n = {0, 0, 0};

    printf("vuelta correo  puntaje resp etiq  w_pal w_enl  base\n");
    int errors = 1;
    int round = 0;
    while (errors > 0 && round < MAX_ROUNDS) {
        round++;
        errors = 0;
        for (int i = 0; i < MAILS; i++) {
            struct example e = examples[i];
            int answer = predict(n, e.words, e.links);
            int current = score(n, e.words, e.links);
            int change = e.spam - answer;   // 0 si acertó; +1 o -1 si se equivocó
            if (change != 0) {
                errors++;
            }
            n.w_words = n.w_words + change * e.words;
            n.w_links = n.w_links + change * e.links;
            n.base = n.base + change;
            printf("%6d (%d,%d) %8d %4d %4d %6d %5d %5d\n", round, e.words, e.links,
                   current, answer, e.spam, n.w_words, n.w_links, n.base);
        }
    }
    printf("Vueltas: %d. Errores en la última: %d\n", round, errors);
    return 0;
}
next_token.cDescargar next_token.c
// Un modelo de lenguaje de juguete. Cuenta en corpus.txt qué palabra viene después de
// cada palabra. Para continuar un texto, elige siempre la siguiente más frecuente, y
// en un empate, la que apareció primero. Lee de la entrada un texto inicial por línea.
#include <stdio.h>
#include <string.h>

#define MAX_LINE 200
#define MAX_WORD 32
#define MAX_PAIRS 100
#define MAX_STEPS 12

struct pair {
    char first[MAX_WORD];
    char second[MAX_WORD];
    int count;
};

// Copia en word la palabra que empieza en line[start] o después.
// Devuelve la posición donde sigue la línea, o -1 si ya no hay palabras.
int read_word_at(const char line[], int start, char word[], int size) {
    int i = start;
    while (line[i] == ' ' || line[i] == '\n') {
        i++;
    }
    if (line[i] == '\0') {
        return -1;
    }
    int length = 0;
    while (line[i] != '\0' && line[i] != ' ' && line[i] != '\n') {
        if (length < size - 1) {
            word[length] = line[i];
            length++;
        }
        i++;
    }
    word[length] = '\0';
    return i;
}

// Suma 1 al par (first, second), o lo agrega. Devuelve el nuevo número de pares.
int add_pair(struct pair pairs[], int n, const char first[], const char second[]) {
    for (int i = 0; i < n; i++) {
        if (strcmp(pairs[i].first, first) == 0 && strcmp(pairs[i].second, second) == 0) {
            pairs[i].count++;
            return n;
        }
    }
    if (n == MAX_PAIRS) {
        return n;
    }
    strcpy(pairs[n].first, first);
    strcpy(pairs[n].second, second);
    pairs[n].count = 1;
    return n + 1;
}

// Entrenar es contar. Devuelve cuántos pares distintos hay, o -1 si no hay archivo.
int train(const char path[], struct pair pairs[]) {
    FILE *file = fopen(path, "r");
    if (file == NULL) {
        perror(path);
        return -1;
    }
    int n = 0;
    char line[MAX_LINE];
    while (fgets(line, MAX_LINE, file) != NULL) {
        char previous[MAX_WORD];
        char word[MAX_WORD];
        int position = read_word_at(line, 0, previous, MAX_WORD);
        while (position != -1) {
            position = read_word_at(line, position, word, MAX_WORD);
            if (position != -1) {
                n = add_pair(pairs, n, previous, word);
                strcpy(previous, word);
            }
        }
    }
    fclose(file);
    return n;
}

// Devuelve la posición del par más frecuente que empieza con word, o -1 si no hay.
int most_frequent(const struct pair pairs[], int n, const char word[]) {
    int best = -1;
    for (int i = 0; i < n; i++) {
        if (strcmp(pairs[i].first, word) == 0) {
            if (best == -1 || pairs[i].count > pairs[best].count) {
                best = i;
            }
        }
    }
    return best;
}

void print_counts(const struct pair pairs[], int n, const char word[]) {
    int total = 0;
    for (int i = 0; i < n; i++) {
        if (strcmp(pairs[i].first, word) == 0) {
            total = total + pairs[i].count;
        }
    }
    printf("tras \"%s\":", word);
    for (int i = 0; i < n; i++) {
        if (strcmp(pairs[i].first, word) == 0) {
            printf(" %s %d/%d", pairs[i].second, pairs[i].count, total);
        }
    }
    printf("\n");
}

// Continúa el texto desde word, hasta el punto o hasta MAX_STEPS palabras.
void generate(const struct pair pairs[], int n, const char word[]) {
    char current[MAX_WORD];
    strcpy(current, word);
    int steps = 0;
    while (strcmp(current, ".") != 0 && steps < MAX_STEPS) {
        int best = most_frequent(pairs, n, current);
        if (best == -1) {
            printf(" [sin datos]");
            return;
        }
        printf(" %s", pairs[best].second);
        strcpy(current, pairs[best].second);
        steps++;
    }
}

int main(void) {
    struct pair pairs[MAX_PAIRS];
    int n = train("corpus.txt", pairs);
    if (n == -1) {
        return 1;
    }

    char line[MAX_LINE];
    while (fgets(line, MAX_LINE, stdin) != NULL) {
        char last[MAX_WORD];
        char word[MAX_WORD];
        int position = read_word_at(line, 0, word, MAX_WORD);
        if (position == -1) {
            continue;
        }
        while (position != -1) {
            strcpy(last, word);
            position = read_word_at(line, position, word, MAX_WORD);
        }
        print_counts(pairs, n, last);
        line[strcspn(line, "\n")] = '\0';
        printf("%s", line);
        generate(pairs, n, last);
        printf("\n");
    }
    return 0;
}
run_checks.cDescargar run_checks.c
// Parte de make test-lab: corre un solo grupo de pruebas de tests.c.
#include <stdio.h>
#include <string.h>

void check_is_prime(void);
void check_max_of_array(void);
void check_copy_text(void);

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "uso: run_checks prime|max|copy\n");
        return 2;
    }
    if (strcmp(argv[1], "prime") == 0) {
        check_is_prime();
    } else if (strcmp(argv[1], "max") == 0) {
        check_max_of_array();
    } else if (strcmp(argv[1], "copy") == 0) {
        check_copy_text();
    } else {
        return 2;
    }
    return 0;
}
spam_models.cDescargar spam_models.c
// Tres modelos para decidir si un correo es spam, a partir de un solo dato: cuántas
// palabras sospechosas tiene. Los correos son inventados para este ejemplo.
#include <stdio.h>

#define TRAIN_SIZE 11
#define TEST_SIZE 8
#define MAX_WORDS 9
#define HAND_THRESHOLD 2

struct mail {
    int words;   // palabras sospechosas
    int spam;    // la etiqueta: 1 si es spam, 0 si no
};

// Regla de umbral: es spam si tiene u palabras sospechosas o más.
int predict_threshold(int words, int u) {
    return words >= u;
}

// Cuenta los errores de la regla de umbral en un conjunto de correos.
int errors_threshold(const struct mail mails[], int n, int u) {
    int errors = 0;
    for (int i = 0; i < n; i++) {
        if (predict_threshold(mails[i].words, u) != mails[i].spam) {
            errors++;
        }
    }
    return errors;
}

// Entrenar: probar cada umbral y quedarse con el primero que comete menos errores.
int train_threshold(const struct mail mails[], int n) {
    int best_u = 0;
    int best_errors = errors_threshold(mails, n, 0);
    for (int u = 1; u <= MAX_WORDS; u++) {
        int errors = errors_threshold(mails, n, u);
        if (errors < best_errors) {
            best_errors = errors;
            best_u = u;
        }
    }
    return best_u;
}

// El memorizador copia la etiqueta del correo de entrenamiento más parecido.
// Si hay empate, se queda con el primero que encontró.
int predict_memorizer(const struct mail train[], int n, int words) {
    int best = 0;
    int best_distance = -1;
    for (int i = 0; i < n; i++) {
        int distance = train[i].words - words;
        if (distance < 0) {
            distance = -distance;
        }
        if (best_distance == -1 || distance < best_distance) {
            best_distance = distance;
            best = i;
        }
    }
    return train[best].spam;
}

int errors_memorizer(const struct mail train[], int n, const struct mail mails[], int m) {
    int errors = 0;
    for (int i = 0; i < m; i++) {
        if (predict_memorizer(train, n, mails[i].words) != mails[i].spam) {
            errors++;
        }
    }
    return errors;
}

int main(void) {
    struct mail train[TRAIN_SIZE] = {
        {0, 0}, {0, 0}, {1, 0}, {1, 0}, {2, 0}, {3, 0}, {4, 1}, {5, 1}, {6, 0}, {7, 1}, {8, 1},
    };
    struct mail test[TEST_SIZE] = {
        {0, 0}, {1, 0}, {2, 0}, {2, 0}, {3, 1}, {4, 1}, {6, 1}, {6, 1},
    };

    printf("Errores en entrenamiento según el umbral\n");
    for (int u = 0; u <= MAX_WORDS; u++) {
        printf("u = %d: %d\n", u, errors_threshold(train, TRAIN_SIZE, u));
    }
    int learned = train_threshold(train, TRAIN_SIZE);
    printf("Umbral aprendido: %d\n\n", learned);

    printf("%-22s %14s %10s\n", "Modelo", "entrenamiento", "prueba");
    printf("%-22s %11d/%d %8d/%d\n", "Regla a mano (u = 2)",
           errors_threshold(train, TRAIN_SIZE, HAND_THRESHOLD), TRAIN_SIZE,
           errors_threshold(test, TEST_SIZE, HAND_THRESHOLD), TEST_SIZE);
    printf("%-22s %11d/%d %8d/%d\n", "Umbral aprendido",
           errors_threshold(train, TRAIN_SIZE, learned), TRAIN_SIZE,
           errors_threshold(test, TEST_SIZE, learned), TEST_SIZE);
    printf("%-22s %11d/%d %8d/%d\n", "Memorizador",
           errors_memorizer(train, TRAIN_SIZE, train, TRAIN_SIZE), TRAIN_SIZE,
           errors_memorizer(train, TRAIN_SIZE, test, TEST_SIZE), TEST_SIZE);
    return 0;
}
suspect_original.cDescargar suspect_original.c
// El código de partida del laboratorio, sin cambios. No lo editen: make test-lab
// comprueba que sus pruebas de lab/tests.c detectan los bugs de este archivo.
//
// Estas funciones se escribieron para esta actividad y tienen bugs a propósito.
// Compilan sin advertencias y pasan una prueba sencilla.

// Devuelve 1 si n es primo y 0 si no.
int is_prime(int n) {
    if (n < 2) {
        return 0;
    }
    for (int i = 2; i * i < n; i++) {
        if (n % i == 0) {
            return 0;
        }
    }
    return 1;
}

// Devuelve el mayor elemento de a. n debe ser al menos 1.
int max_of_array(const int a[], int n) {
    int best = 0;
    for (int i = 0; i < n; i++) {
        if (a[i] > best) {
            best = a[i];
        }
    }
    return best;
}

// Copia src en dest, que tiene size bytes. Si no cabe, copia lo que quepa.
// dest siempre termina en '\0'. size debe ser al menos 1.
void copy_text(char dest[], const char src[], int size) {
    int i = 0;
    while (src[i] != '\0' && i < size) {
        dest[i] = src[i];
        i++;
    }
    dest[i] = '\0';
}

Errores

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

errors/last_char.cDescargar errors/last_char.c
// Pasa la prueba con "hola", y falla con la cadena vacía: lee s[-1], fuera del arreglo.
#include <stdio.h>
#include <string.h>

char last_char(const char s[]) {
    int n = strlen(s);
    return s[n - 1];
}

int main(void) {
    char word[] = "hola";
    char empty[] = "";
    printf("%c\n", last_char(word));
    fflush(stdout);    // para que se vea antes del informe del sanitizer
    printf("%c\n", last_char(empty));
    return 0;
}
errors/passing_border.cDescargar errors/passing_border.c
// La función de la transcripción A, con la prueba que encuentra su bug: una nota de 11.
#include <assert.h>
#include <stdio.h>

// Cuenta cuántas notas están aprobadas. Se aprueba con 11 o más.
int count_passing(const int grades[], int n) {
    int count = 0;
    for (int i = 0; i < n; i++) {
        if (grades[i] > 11) {
            count++;
        }
    }
    return count;
}

int main(void) {
    int grades[5] = {14, 9, 17, 8, 20};
    assert(count_passing(grades, 5) == 3);

    int border[3] = {11, 10, 12};
    assert(count_passing(border, 3) == 2);
    printf("Las dos pruebas pasaron\n");
    return 0;
}

Pruebas

Comprueban lo que afirman los apuntes; se ejecutan con make test.

test_examples.pyDescargar test_examples.py
"""Comprueba lo que los apuntes afirman sobre los programas de la semana 15.

Los resultados esperados se recalculan aquí, con los datos del código, de forma independiente.
"""
import pathlib
import re
import subprocess
import tempfile
import unittest

HERE = pathlib.Path(__file__).parent
BUILD = HERE / "build"
FLAGS = ["-Wall", "-Wextra", "-std=c17"]
SANITIZE = ["-g", "-fsanitize=address,undefined"]


def execute(command, stdin="", cwd=HERE):
    done = subprocess.run(
        command, input=stdin, cwd=cwd, encoding="utf-8", errors="replace",
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=20,
    )
    return done.stdout, done.returncode


def run(program, stdin=""):
    return execute([str(BUILD / program)], stdin)


def build_source(source, *extra):
    """Compila un archivo; devuelve los mensajes, el estado y el ejecutable."""
    out = pathlib.Path(tempfile.mkdtemp()) / pathlib.Path(source).stem
    output, code = execute(["gcc", *FLAGS, *extra, source, "-o", str(out)])
    return output, code, out


def mail_data(name):
    """Lee del código fuente los pares (palabras, etiqueta) del arreglo indicado."""
    source = (HERE / "spam_models.c").read_text(encoding="utf-8")
    block = re.search(r"struct mail %s\[\w+\] = \{(.*?)\n    \};" % name, source, re.S).group(1)
    return [(int(a), int(b)) for a, b in re.findall(r"\{(\d+), (\d+)\}", block)]


def threshold_errors(mails, u):
    return sum(1 for words, spam in mails if int(words >= u) != spam)


def memorizer_errors(train, mails):
    errors = 0
    for words, spam in mails:
        closest = min(range(len(train)), key=lambda i: (abs(train[i][0] - words), i))
        errors += train[closest][1] != spam
    return errors


class CleanCompile(unittest.TestCase):
    def test_examples_have_no_warnings(self):
        for source in sorted(HERE.glob("*.c")) + sorted((HERE / "errors").glob("*.c")):
            with self.subTest(source=source.name):
                output, code = execute(["gcc", *FLAGS, "-Werror", "-fsyntax-only", str(source)])
                self.assertEqual(code, 0, output)


class SpamModels(unittest.TestCase):
    def setUp(self):
        self.train = mail_data("train")
        self.test = mail_data("test")
        self.output, self.code = run("spam_models")

    def test_runs(self):
        self.assertEqual(self.code, 0)
        self.assertEqual((len(self.train), len(self.test)), (11, 8))

    def test_error_table_matches_the_data(self):
        for u in range(10):
            self.assertIn(f"u = {u}: {threshold_errors(self.train, u)}\n", self.output)

    def test_learned_threshold_has_the_fewest_errors(self):
        errors = [threshold_errors(self.train, u) for u in range(10)]
        self.assertIn(f"Umbral aprendido: {errors.index(min(errors))}\n", self.output)
        self.assertEqual(errors.index(min(errors)), 4)

    def row(self, name):
        line = next(l for l in self.output.splitlines() if l.startswith(name) and "/" in l)
        return re.findall(r"(\d+)/\d+", line.replace("(u = 2)", ""))

    def test_summary_matches_the_data(self):
        expected = {
            "Regla a mano": (threshold_errors(self.train, 2), threshold_errors(self.test, 2)),
            "Umbral aprendido": (threshold_errors(self.train, 4), threshold_errors(self.test, 4)),
            "Memorizador": (memorizer_errors(self.train, self.train),
                            memorizer_errors(self.train, self.test)),
        }
        for name, (train, test) in expected.items():
            with self.subTest(model=name):
                self.assertEqual(self.row(name), [str(train), str(test)])

    def test_the_memorizer_overfits(self):
        # Lo que dicen los apuntes: el mejor en entrenamiento es el peor en prueba.
        self.assertEqual(memorizer_errors(self.train, self.train), 0)
        self.assertGreater(memorizer_errors(self.train, self.test),
                           threshold_errors(self.test, 4))


class Neuron(unittest.TestCase):
    def setUp(self):
        self.output, self.code = run("neuron")
        self.rows = [tuple(int(x) for x in re.findall(r"-?\d+", line))
                     for line in self.output.splitlines() if "(" in line]

    def test_each_row_follows_the_rule(self):
        self.assertEqual(self.code, 0)
        w_words, w_links, base = 0, 0, 0
        for _, words, links, score, answer, label, new_words, new_links, new_base in self.rows:
            self.assertEqual(score, w_words * words + w_links * links + base)
            self.assertEqual(answer, int(score > 0))
            change = label - answer
            w_words += change * words
            w_links += change * links
            base += change
            self.assertEqual((new_words, new_links, new_base), (w_words, w_links, base))

    def test_last_round_has_no_errors(self):
        self.assertIn("Vueltas: 3. Errores en la última: 0\n", self.output)
        for row in self.rows[-4:]:
            self.assertEqual(row[4], row[5])
        self.assertEqual(self.rows[-1][-3:], (1, 1, -2))


class Network(unittest.TestCase):
    def test_truth_table_is_xor(self):
        output, code = run("network")
        self.assertEqual(code, 0)
        rows = [line.split() for line in output.splitlines() if re.match(r"[01] [01]", line)]
        self.assertEqual(len(rows), 4)
        for row in rows:
            a, b, first, second, result = (int(x) for x in row)
            self.assertEqual(first, int(a or b))
            self.assertEqual(second, int(not (a and b)))
            self.assertEqual(result, int(a != b))

    def test_no_single_neuron_in_the_range(self):
        count = 0
        for w_a in range(-3, 4):
            for w_b in range(-3, 4):
                for base in range(-3, 4):
                    fire = lambda a, b: int(w_a * a + w_b * b + base > 0)
                    count += all(fire(a, b) == int(a != b) for a in (0, 1) for b in (0, 1))
        output, _ = run("network")
        self.assertIn(f"{count} de 343 combinaciones", output)
        self.assertEqual(count, 0)


class NextToken(unittest.TestCase):
    def setUp(self):
        self.lines = (HERE / "corpus.txt").read_text(encoding="utf-8").splitlines()

    def counts_after(self, word):
        found = {}
        for line in self.lines:
            tokens = line.split()
            for a, b in zip(tokens, tokens[1:]):
                if a == word:
                    found[b] = found.get(b, 0) + 1
        return found

    def test_counts_come_from_the_corpus(self):
        for word in ["es", "la", "de", "publicó", "sobre", "en"]:
            found = self.counts_after(word)
            total = sum(found.values())
            output, code = run("next_token", word + "\n")
            self.assertEqual(code, 0)
            first = output.splitlines()[0]
            for following, count in found.items():
                with self.subTest(word=word, following=following):
                    self.assertIn(f"{following} {count}/{total}", first)

    def answer(self, prompt):
        output, code = run("next_token", prompt + "\n")
        self.assertEqual(code, 0)
        return output.splitlines()[1]

    def test_same_answer_true_or_false(self):
        self.assertEqual(self.answer("la capital de perú es"), "la capital de perú es lima .")
        self.assertEqual(self.answer("la capital de chile es"), "la capital de chile es lima .")
        self.assertEqual(self.answer("la capital de ecuador es"), "la capital de ecuador es lima .")
        self.assertIn("la capital de chile es santiago .", self.lines)
        self.assertFalse(any("ecuador" in line for line in self.lines))

    def test_invented_citation_is_not_in_the_corpus(self):
        made_up = self.answer("quispe publicó")
        self.assertEqual(made_up, "quispe publicó un artículo sobre redes en 2019 .")
        self.assertNotIn(made_up, self.lines)
        self.assertIn(self.answer("flores publicó"), self.lines)

    def test_no_data(self):
        self.assertEqual(self.answer("ecuador"), "ecuador [sin datos]")

    def test_blank_lines_are_skipped(self):
        self.assertEqual(run("next_token", "\n   \n")[0], "")

    def test_missing_corpus_is_reported(self):
        output, code = execute([str(BUILD / "next_token")], cwd=tempfile.mkdtemp())
        self.assertEqual(code, 1)
        self.assertIn("corpus.txt", output)


class TheOriginalHasTheBugs(unittest.TestCase):
    def call(self, *args):
        return execute([str(BUILD / "check_original"), *args])

    def test_passes_the_simple_cases(self):
        self.assertEqual(self.call("prime", "7")[0], "1\n")
        self.assertEqual(self.call("prime", "8")[0], "0\n")
        self.assertEqual(self.call("max", "14", "9", "17")[0], "17\n")
        self.assertEqual(self.call("copy", "5", "hola")[0], "[hola]\n")

    def test_fails_at_the_edges(self):
        self.assertEqual(self.call("prime", "9")[0], "1\n")
        self.assertEqual(self.call("max", "-4", "-2", "-9")[0], "0\n")
        output, code = self.call("copy", "3", "hola")
        self.assertNotEqual(code, 0)
        self.assertIn("AddressSanitizer: stack-buffer-overflow", output)


class Transcripts(unittest.TestCase):
    TEXT = (HERE / "lab" / "transcripts.md").read_text(encoding="utf-8")

    def test_each_one_is_labelled_as_an_example(self):
        self.assertIn("No son la salida real de ningún asistente", self.TEXT)
        headings = [l for l in self.TEXT.splitlines() if l.startswith("## ")]
        self.assertEqual(len(headings), 3)
        for heading in headings:
            self.assertIn("ejemplo escrito para la actividad", heading)

    def test_the_code_in_the_text_is_the_code_in_the_folder(self):
        known = set()
        for source in (HERE / "lab" / "transcripts").glob("*.c"):
            known.update(l.strip() for l in source.read_text(encoding="utf-8").splitlines())
        for block in re.findall(r"```c\n(.*?)```", self.TEXT, re.S):
            for line in block.splitlines():
                with self.subTest(line=line):
                    self.assertIn(line.strip(), known | {""})

    def run_transcript(self, name):
        output, code, program = build_source(f"lab/transcripts/{name}.c", *SANITIZE)
        self.assertEqual((output, code), ("", 0))
        return execute([str(program)])

    def test_a_passes_its_own_test_and_has_a_bug(self):
        self.assertEqual(self.run_transcript("count_passing"), ("Pruebas pasadas\n", 0))
        output, code, program = build_source("errors/passing_border.c", *SANITIZE)
        self.assertEqual((output, code), ("", 0))
        output, code = execute([str(program)])
        self.assertNotEqual(code, 0)
        self.assertIn("Assertion `count_passing(border, 3) == 2' failed", output)

    def test_b_is_correct_where_it_can_be_checked(self):
        self.assertEqual(self.run_transcript("count_char"), ("Pruebas pasadas\n", 0))
        source = (HERE / "lab" / "transcripts" / "count_char.c").read_text(encoding="utf-8")
        self.assertIn('assert(count_char("banana", \'a\') == 3);', source)
        checked = source.replace('assert(count_char("banana", \'a\') == 3);', """
            assert(count_char("", 'a') == 0);
            assert(count_char("xyz", 'a') == 0);
            assert(count_char("ab", 'a') == 1);
            assert(count_char("ba", 'a') == 1);
            assert(count_char("aaa", 'a') == 3);
        """)
        with tempfile.TemporaryDirectory() as folder:
            path = pathlib.Path(folder) / "checked.c"
            path.write_text(checked, encoding="utf-8")
            output, code, program = build_source(str(path), *SANITIZE)
            self.assertEqual((output, code), ("", 0))
            self.assertEqual(execute([str(program)]), ("Pruebas pasadas\n", 0))

    def test_c_does_not_compile(self):
        output, code, _ = build_source("lab/transcripts/reverse_text.c", *SANITIZE)
        self.assertNotEqual(code, 0)
        self.assertIn("implicit declaration of function", output)
        self.assertIn("strrev", output)

    def test_the_gcc_output_in_the_text_is_real(self):
        output, _, _ = build_source("lab/transcripts/reverse_text.c", *SANITIZE)
        normalized = output.replace("‘", "'").replace("’", "'").strip()
        text = self.TEXT.replace("‘", "'").replace("’", "'")
        self.assertIn(normalized, text)


class LastChar(unittest.TestCase):
    def test_works_with_hola_and_is_caught_with_the_empty_string(self):
        output, code, program = build_source("errors/last_char.c", *SANITIZE)
        self.assertEqual((output, code), ("", 0))
        done = subprocess.run([str(program)], encoding="utf-8", errors="replace",
                              stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=10)
        self.assertTrue(done.stdout.startswith("a\n"))
        self.assertNotEqual(done.returncode, 0)
        self.assertIn("AddressSanitizer", done.stderr)


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.

Se prueban dos cosas: que las funciones de suspect.c estén corregidas, y que las pruebas
de tests.c sirvan. Una prueba sirve si pasa con el código corregido y falla con el original.
"""
import os
import pathlib
import subprocess
import unittest

HERE = pathlib.Path(__file__).parent
LAB_DIR = os.environ.get("LAB_DIR", "solution")
GROUPS = {"prime": "is_prime", "max": "max_of_array", "copy": "copy_text"}


def run(program, *args):
    done = subprocess.run(
        [str(HERE / "build" / LAB_DIR / program), *args], encoding="utf-8", errors="replace",
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=10,
    )
    return done.stdout, done.returncode


def answer(*args):
    output, code = run("check_suspect", *args)
    # El programa se detiene con un informe si la función se sale del arreglo.
    return output.strip() if code == 0 else f"falló con estado {code}"


@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe {LAB_DIR}/")
class FixedFunctions(unittest.TestCase):
    def test_no_warnings(self):
        for name in ["suspect", "tests"]:
            with self.subTest(name=name):
                done = subprocess.run(
                    ["gcc", "-Wall", "-Wextra", "-std=c17", "-Werror", "-fsyntax-only",
                     str(HERE / LAB_DIR / f"{name}.c")],
                    text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                )
                self.assertEqual(done.returncode, 0, done.stdout)

    def test_is_prime(self):
        cases = {-7: 0, 0: 0, 1: 0, 2: 1, 3: 1, 4: 0, 5: 1, 9: 0, 13: 1, 15: 0, 25: 0,
                 49: 0, 97: 1, 121: 0, 169: 0, 361: 0, 1369: 0, 7919: 1}
        for n, expected in cases.items():
            with self.subTest(n=n):
                self.assertEqual(answer("prime", str(n)), str(expected))

    def test_max_of_array(self):
        cases = [(["5"], "5"), (["-5"], "-5"), (["0"], "0"), (["14", "9", "17"], "17"),
                 (["20", "9", "17"], "20"), (["9", "20", "17"], "20"), (["-4", "-2", "-9"], "-2"),
                 (["-1", "-1"], "-1"), (["-9", "0", "-3"], "0"), (["3", "3", "3"], "3")]
        for numbers, expected in cases:
            with self.subTest(numbers=numbers):
                self.assertEqual(answer("max", *numbers), expected)

    def test_copy_text(self):
        cases = [("5", "hola", "[hola]"), ("5", "", "[]"), ("5", "luna!", "[luna]"),
                 ("5", "estrella", "[estr]"), ("3", "hola", "[ho]"), ("3", "ab", "[ab]"),
                 ("3", "abc", "[ab]"), ("2", "xy", "[x]"), ("1", "hola", "[]"), ("1", "", "[]")]
        for size, text, expected in cases:
            with self.subTest(size=size, text=text):
                self.assertEqual(answer("copy", size, text), expected)


@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe {LAB_DIR}/")
class YourTests(unittest.TestCase):
    def test_your_program_passes(self):
        output, code = run("tests")
        self.assertEqual((output, code), ("Todas las pruebas pasaron\n", 0),
                         "sus pruebas deben pasar con su suspect.c corregido")

    def test_each_group_passes_on_your_fixed_code(self):
        for group, name in GROUPS.items():
            with self.subTest(function=name):
                output, code = run("tests_fixed", group)
                self.assertEqual(code, 0, f"sus pruebas de {name} fallan con su código:\n{output}")

    def test_each_group_finds_the_bug_of_the_original(self):
        for group, name in GROUPS.items():
            with self.subTest(function=name):
                _, code = run("tests_original", group)
                self.assertNotEqual(
                    code, 0, f"sus pruebas de {name} pasan con el original, que tiene un bug")


if __name__ == "__main__":
    unittest.main()
test_notes.pyDescargar test_notes.py
"""Vuelve a correr los comandos que aparecen en los apuntes y en las diapositivas
y comprueba que el código citado existe de verdad en esta carpeta."""
import pathlib
import re
import subprocess
import unittest

HERE = pathlib.Path(__file__).parent
ROOT = HERE.parent.parent
NOTES = ROOT / "notes" / "week-15.md"
SLIDES = ROOT / "slides" / "week-15.tex"
# En esta salida, solo las comillas de gcc cambian según el idioma del entorno.


def normalize(text):
    return text.replace("‘", "'").replace("’", "'")


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


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 las líneas de cada bloque de los apuntes."""
    blocks, current = [], 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 = []
        elif line.strip() == "```":
            blocks.append(current)
            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()
    for source in list(HERE.glob("*.c")) + list((HERE / "errors").glob("*.c")):
        lines.update(line.strip() for line in source.read_text(encoding="utf-8").splitlines())
    return lines


class NotesAreTrue(unittest.TestCase):
    @unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
    def test_terminal_blocks_in_notes(self):
        checked = 0
        for lines in note_blocks("text"):
            if not lines or not lines[0].startswith("$ "):
                continue
            for command, expected in split_commands(lines):
                actual = run_shell(command).rstrip("\n")
                checked += 1
                with self.subTest(command=command):
                    output = [line.rstrip() for line in actual.split("\n")]
                    self.assertEqual("\n".join(output).strip(), "\n".join(expected).strip())
        self.assertGreater(checked, 5)

    @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")
    def test_tables_match_the_program(self):
        text = NOTES.read_text(encoding="utf-8")
        output = run_shell("build/spam_models")
        rows = {"Regla a mano (`u = 2`)": "Regla a mano", "Umbral aprendido (`u = 4`)": "Umbral aprendido",
                "Memorizador": "Memorizador"}
        for row, name in rows.items():
            line = next(l for l in output.splitlines() if l.startswith(name) and "/" in l)
            train, train_n, test, test_n = re.findall(r"\d+", line.replace("(u = 2)", ""))
            pattern = r"\| %s \| \**%s de %s\** \| \**%s de %s\** \|" % (
                re.escape(row), train, train_n, test, test_n)
            with self.subTest(row=row):
                self.assertRegex(text, pattern)

    @unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
    def test_examples_are_not_presented_as_real_runs(self):
        flat = " ".join(NOTES.read_text(encoding="utf-8").split())
        self.assertIn("ejemplos escritos para esta actividad, no salidas reales de un asistente", flat)

    @unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
    def test_policy_matches_the_syllabus(self):
        syllabus = (ROOT / "SYLLABUS.md").read_text(encoding="utf-8")
        self.assertIn("prohibidas en el curso, salvo en la actividad guiada de la semana 15", syllabus)
        flat = " ".join(NOTES.read_text(encoding="utf-8").replace("\n> ", " ").split())
        self.assertIn("están **prohibidas** en el curso, salvo en la actividad guiada", flat)

    @unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
    def test_no_urls(self):
        for text in [NOTES.read_text(encoding="utf-8")] + (
                [SLIDES.read_text(encoding="utf-8")] if SLIDES.exists() else []):
            self.assertNotRegex(text, r"https?://")


class SlidesAreTrue(unittest.TestCase):
    @unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
    def test_terminal_blocks_in_slides(self):
        for block in slide_blocks("terminal|salidalarga"):
            for command, expected in split_commands(block):
                actual = run_shell(command).splitlines()
                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/activity_sheet.mdDescargar lab/activity_sheet.md
# Hoja de la actividad guiada

Copien esta hoja en un archivo o en papel, una por cada función que revisen. Esta es la única actividad del
curso donde se permite usar IA generativa.

**Antes de escribir un prompt:** no pongan datos personales, ni suyos ni de otras personas, y tampoco código de sus
compañeros.

1. **Función que pidieron** (o transcripción que usaron):
2. **Prompt** exacto que escribieron:
3. **Asistente:** cuál usaron, o "transcripción A, B o C", que son ejemplos escritos para la actividad.
4. **¿Qué debería hacer la función?** Escríbanlo antes de leer la respuesta, con sus casos borde:

   | Entrada | Resultado esperado | ¿Por qué ese caso? |
   |---|---|---|
   | | | |
   | | | |
   | | | |

5. **Lectura.** ¿Qué líneas entienden? ¿Cuáles no? Lo que no puedan explicar, no lo den por bueno.
6. **Compilación** con `gcc -Wall -Wextra -std=c17 -g -fsanitize=address,undefined`. ¿Hubo errores o advertencias?
7. **Pruebas `assert`.** ¿Cuáles pasaron y cuáles fallaron? Sin computadora, escriban la tabla de variables.
8. **Qué hizo bien** la respuesta:
9. **Qué error encontraron**, o "ninguno en estos casos":
10. **Cómo lo detectaron:** la lectura, `gcc`, un `assert`, el sanitizer o una tabla a mano.
lab/suspect.cDescargar lab/suspect.c
// Laboratorio, parte 1: tres funciones sospechosas.
//
// Estas funciones se escribieron para esta actividad y tienen bugs a propósito: una
// por función. Compilan sin advertencias y pasan la prueba de lab/tests.c. Es lo que
// puede pasarles con el código de cualquiera, o con la respuesta de un asistente.
//
// No las corrijan todavía. Primero escriban en lab/tests.c una prueba que falle.
// Después corrijan aquí la función y comprueben que la prueba pasa.
//
// Pruébenlo con make test-lab.

// Devuelve 1 si n es primo y 0 si no.
int is_prime(int n) {
    if (n < 2) {
        return 0;
    }
    for (int i = 2; i * i < n; i++) {
        if (n % i == 0) {
            return 0;
        }
    }
    return 1;
}

// Devuelve el mayor elemento de a. n debe ser al menos 1.
int max_of_array(const int a[], int n) {
    int best = 0;
    for (int i = 0; i < n; i++) {
        if (a[i] > best) {
            best = a[i];
        }
    }
    return best;
}

// Copia src en dest, que tiene size bytes. Si no cabe, copia lo que quepa.
// dest siempre termina en '\0'. size debe ser al menos 1.
void copy_text(char dest[], const char src[], int size) {
    int i = 0;
    while (src[i] != '\0' && i < size) {
        dest[i] = src[i];
        i++;
    }
    dest[i] = '\0';
}
lab/tests.cDescargar lab/tests.c
// Laboratorio, parte 1: sus pruebas de las funciones de lab/suspect.c.
//
// Cada función de abajo trae una prueba, y las tres pasan aunque suspect.c tiene bugs.
// Agreguen asserts hasta que cada grupo encuentre el bug de su función. Piensen en los
// bordes. Para copy_text, usen un destino exactamente del tamaño que le pasan: el
// sanitizer solo puede avisar si la escritura se sale de ese arreglo.
//
// Compilen y corran con:
//     make build/lab/tests && build/lab/tests
#include <assert.h>
#include <stdio.h>
#include <string.h>

int is_prime(int n);
int max_of_array(const int a[], int n);
void copy_text(char dest[], const char src[], int size);

void check_is_prime(void) {
    assert(is_prime(7) == 1);
    assert(is_prime(8) == 0);
}

void check_max_of_array(void) {
    int grades[3] = {14, 9, 17};
    assert(max_of_array(grades, 3) == 17);
}

void check_copy_text(void) {
    char word[20];
    copy_text(word, "hola", 20);
    assert(strcmp(word, "hola") == 0);
}

int main(void) {
    check_is_prime();
    check_max_of_array();
    check_copy_text();
    printf("Todas las pruebas pasaron\n");
    return 0;
}
lab/transcripts.mdDescargar lab/transcripts.md
# Transcripciones para la actividad guiada

Úsenlas si en el laboratorio no hay un asistente permitido o no hay internet.

**Son ejemplos escritos para esta actividad. No son la salida real de ningún asistente.** Sirven
para practicar lo mismo que con uno real: leer la respuesta sin creerla, y comprobarla. Las salidas de `gcc` y de los programas
que aparecen aquí sí son reales, del entorno del curso.

Con cada transcripción, sigan los pasos de la hoja `lab/activity_sheet.md`. El código está en
`lab/transcripts/`. Sin computadora, sigan la función a mano en papel, con una tabla de variables.

## Transcripción A (ejemplo escrito para la actividad)

**Estudiante:** Escribe en C una función que cuente cuántas notas de un arreglo están aprobadas.
Se aprueba con 11 o más, en escala de 0 a 20.

**Asistente:** Claro. Esta función es correcta para cualquier arreglo de notas:

```c
int count_passing(const int grades[], int n) {
    int count = 0;
    for (int i = 0; i < n; i++) {
        if (grades[i] > 11) {
            count++;
        }
    }
    return count;
}
```

Y aquí tienes una prueba, que pasa:

```c
int grades[5] = {14, 9, 17, 8, 20};
assert(count_passing(grades, 5) == 3);
```

**Salida real** con `lab/transcripts/count_passing.c`:

```text
Pruebas pasadas
```

**Para ustedes:** ¿basta con esa prueba? Escojan notas que busquen un error, y sigan la función a mano.

## Transcripción B (ejemplo escrito para la actividad)

**Estudiante:** Escribe en C una función que cuente cuántas veces aparece un byte en una cadena.

**Asistente:** Aquí está. Recorre la cadena hasta el `'\0'`:

```c
int count_char(const char s[], char c) {
    int count = 0;
    for (int i = 0; s[i] != '\0'; i++) {
        if (s[i] == c) {
            count++;
        }
    }
    return count;
}
```

```c
assert(count_char("banana", 'a') == 3);
```

**Salida real** con `lab/transcripts/count_char.c`:

```text
Pruebas pasadas
```

**Para ustedes:** ¿qué casos faltan por probar? Piensen en la cadena vacía, en un byte que no aparece y en
uno que está al principio o al final. Decidan si encuentran un error. No todas las respuestas lo tienen,
y sus pruebas deben poder decir eso también, hasta donde pueden: una prueba muestra lo que pasa solo en los casos que revisa.

## Transcripción C (ejemplo escrito para la actividad)

**Estudiante:** ¿Cómo invierto una cadena en C?

**Asistente:** Es muy fácil. La biblioteca estándar lo trae en `string.h`:

```c
char word[] = "hola";
strrev(word);
printf("%s\n", word);
```

**Salida real** de `gcc -Wall -Wextra -std=c17 -g -fsanitize=address,undefined` con `lab/transcripts/reverse_text.c`:

```text
lab/transcripts/reverse_text.c: In function ‘main’:
lab/transcripts/reverse_text.c:7:5: error: implicit declaration of function ‘strrev’; did you mean ‘strlen’? [-Wimplicit-function-declaration]
    7 |     strrev(word);
      |     ^~~~~~
      |     strlen
```

**Para ustedes:** ¿qué dice el mensaje? `strrev` no es parte del estándar de C, aunque la respuesta sonaba
segura y algunos entornos la traen como extra. Escriban la función a mano. En la semana 10 hay un ejercicio que la pide.
lab/transcripts/count_char.cDescargar lab/transcripts/count_char.c
// Código de la transcripción B de lab/transcripts.md. Es un ejemplo escrito para la actividad.
#include <assert.h>
#include <stdio.h>

// Cuenta cuántas veces aparece el byte c en la cadena s.
int count_char(const char s[], char c) {
    int count = 0;
    for (int i = 0; s[i] != '\0'; i++) {
        if (s[i] == c) {
            count++;
        }
    }
    return count;
}

int main(void) {
    assert(count_char("banana", 'a') == 3);
    printf("Pruebas pasadas\n");
    return 0;
}
lab/transcripts/count_passing.cDescargar lab/transcripts/count_passing.c
// Código de la transcripción A de lab/transcripts.md. Es un ejemplo escrito para la actividad.
#include <assert.h>
#include <stdio.h>

// Cuenta cuántas notas están aprobadas. Se aprueba con 11 o más.
int count_passing(const int grades[], int n) {
    int count = 0;
    for (int i = 0; i < n; i++) {
        if (grades[i] > 11) {
            count++;
        }
    }
    return count;
}

int main(void) {
    int grades[5] = {14, 9, 17, 8, 20};
    assert(count_passing(grades, 5) == 3);
    printf("Pruebas pasadas\n");
    return 0;
}
lab/transcripts/reverse_text.cDescargar lab/transcripts/reverse_text.c
// Código de la transcripción C de lab/transcripts.md. Es un ejemplo escrito para la actividad.
#include <stdio.h>
#include <string.h>

int main(void) {
    char word[] = "hola";
    strrev(word);
    printf("%s\n", word);
    return 0;
}

Compilación

Cómo se compilan y ejecutan los ejemplos.

MakefileDescargar Makefile
# Semana 15: inteligencia artificial y ética. Se usa dentro del entorno del curso.
#
#   make           compila los programas de la clase en build/
#   make test      corre las pruebas de los ejemplos y de los apuntes
#   make test-lab  prueba su laboratorio (lab/suspect.c y lab/tests.c); falla hasta que lo terminen
#   make clean     borra lo generado
#
# Los programas de esta semana no son para escribirlos: sirven para seguir a mano
# las ideas del capítulo.
#
# El laboratorio usa varios archivos. Repartir un programa así se aprende en cursos
# posteriores: aquí lo hace make. Con -Dmain=student_main, el main de tests.c se
# compila con otro nombre, y run_checks.c corre cada grupo de pruebas por separado.
# Se enlazan dos versiones: con su suspect.c corregido y con suspect_original.c.

CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -g -fsanitize=address,undefined
BUILD = build
PROGRAMS = spam_models neuron network next_token

.PHONY: all lab test test-lab clean

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

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

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

# Las funciones del original, con las pruebas del laboratorio (sirve para ver que los bugs existen).
$(BUILD)/check_original: check_suspect.c suspect_original.c | $(BUILD)
	$(CC) $(CFLAGS) check_suspect.c suspect_original.c -o $@

define LAB_RULES
$(BUILD)/$(1)/check_suspect: check_suspect.c $(1)/suspect.c | $(BUILD)/$(1)
	$(CC) $(CFLAGS) check_suspect.c $(1)/suspect.c -o $$@

$(BUILD)/$(1)/tests_fixed: run_checks.c $(1)/tests.c $(1)/suspect.c | $(BUILD)/$(1)
	$(CC) $(CFLAGS) -Dmain=student_main -c $(1)/tests.c -o $(BUILD)/$(1)/tests.o
	$(CC) $(CFLAGS) run_checks.c $(BUILD)/$(1)/tests.o $(1)/suspect.c -o $$@

$(BUILD)/$(1)/tests_original: run_checks.c $(1)/tests.c suspect_original.c | $(BUILD)/$(1)
	$(CC) $(CFLAGS) -Dmain=student_main -c $(1)/tests.c -o $(BUILD)/$(1)/tests.o
	$(CC) $(CFLAGS) run_checks.c $(BUILD)/$(1)/tests.o suspect_original.c -o $$@

$(BUILD)/$(1)/tests: $(1)/tests.c $(1)/suspect.c | $(BUILD)/$(1)
	$(CC) $(CFLAGS) $(1)/tests.c $(1)/suspect.c -o $$@
endef

$(eval $(call LAB_RULES,lab))
$(eval $(call LAB_RULES,solution))

LAB_FILES = check_suspect tests_fixed tests_original tests

lab: $(LAB_FILES:%=$(BUILD)/lab/%)

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