Semana 3 — El modelo de entornos

Objetivos

  • Dibujar los marcos de llamada de un programa mientras se ejecuta.
  • Distinguir alcance y tiempo de vida de una variable.
  • Inspeccionar marcos reales con gdb.

Teoría — El modelo de entornos: marcos, alcance y tiempo de vida

Lectura: Apuntes, semana 3.

Temas:

  • Por qué hace falta un modelo: predecir qué hace un programa sin ejecutarlo.
  • Cada llamada crea un marco con sus parámetros y variables locales. Al retornar, el marco desaparece.
  • Alcance: dónde se puede usar un nombre. Tiempo de vida: cuánto existe el dato.
  • Variables locales, static y constantes globales. La pila de llamadas como una secuencia de marcos.
  • Paso por valor visto en el modelo, y por qué pasar un arreglo se comporta distinto.

Laboratorio — Imágenes: filtros y marcos en gdb

Las imágenes en escala de grises se guardan en formato PGM de texto, y aquí tienen un tamaño máximo fijo.

Sesión 1:

  1. Dibujar a mano los marcos de tres programas dados, y comprobar el dibujo en gdb con break, backtrace, frame e info locals.
  2. Leer el módulo dado que carga y guarda imágenes PGM.

Sesión 2: 3. Escribir los filtros invert, threshold y flip_horizontal, con pruebas sobre imágenes pequeñas. 4. Escribir box_blur y resolver qué hacer en los bordes.

Preguntas de repaso

  1. Dibujen los marcos cuando main llama a f y f llama a g. ¿Cuáles existen dentro de g?
  2. ¿Qué diferencia hay entre el alcance y el tiempo de vida de una variable static local?
  3. ¿Por qué void inc(int x) { x++; } no cambia la variable de quien llama?
  4. ¿Por qué una función sí puede cambiar los elementos de un arreglo que recibe?

Código de la semana

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

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

Ejemplos

Los programas que usan los apuntes y la clase.

array_param.cDescargar array_param.c
// Se copia la dirección del arreglo, no sus elementos.
#include <stdio.h>

#define SIZE 4

void change_int(int x) {
    x = 99;
    printf("en change_int: x = %d\n", x);
}

void change_array(int values[], int n) {
    for (int i = 0; i < n; i++) {
        values[i] = values[i] * 10;
    }
    n = 0;
    printf("en change_array: n = %d\n", n);
}

void print_array(const int values[], int n) {
    for (int i = 0; i < n; i++) {
        printf("%d ", values[i]);
    }
    printf("\n");
}

int main(void) {
    int count = SIZE;
    int data[SIZE] = {1, 2, 3, 4};

    change_int(data[0]);
    print_array(data, count);

    change_array(data, count);
    print_array(data, count);
    printf("count = %d\n", count);
    return 0;
}
blocks.cDescargar blocks.c
// Un mismo nombre en bloques distintos, y una variable que nace en cada vuelta.
#include <stdio.h>

int main(void) {
    int x = 1;
    printf("afuera: x = %d\n", x);

    {
        int x = 2;
        x = x + 10;
        printf("adentro: x = %d\n", x);
    }
    printf("afuera otra vez: x = %d\n", x);

    int total = 0;
    for (int i = 1; i <= 3; i++) {
        int square = i * i;
        total = total + square;
    }
    printf("total = %d\n", total);
    return 0;
}
by_value.cDescargar by_value.c
// El parámetro es una copia: lo que cambia inc no le llega a main.
#include <stdio.h>

void inc(int x) {
    x++;
    printf("en inc: x = %d\n", x);
}

int plus_one(int x) {
    return x + 1;
}

int main(void) {
    int x = 7;

    inc(x);
    printf("en main: x = %d\n", x);

    x = plus_one(x);
    printf("con return: x = %d\n", x);
    return 0;
}
frames.cDescargar frames.c
// Tres marcos: main llama a add_triple, y add_triple llama a triple.
#include <stdio.h>

int triple(int n) {
    int result = n * 3;
    return result;
}

int add_triple(int a, int b) {
    int t = triple(b);
    a = a + t;
    return a;
}

int main(void) {
    int a = 2;
    int b = 5;
    int c = add_triple(a, b);

    printf("a = %d, b = %d, c = %d\n", a, b, c);
    return 0;
}
global_constant.cDescargar global_constant.c
// Una constante global: todas las funciones la ven y ninguna la puede cambiar.
#include <stdio.h>

#define MAX_STUDENTS 4

const int PASSING_GRADE = 11;

int is_passing(int grade) {
    return grade >= PASSING_GRADE;
}

int count_passing(const int grades[], int n) {
    int count = 0;
    for (int i = 0; i < n; i++) {
        if (is_passing(grades[i])) {
            count = count + 1;
        }
    }
    return count;
}

int main(void) {
    int grades[MAX_STUDENTS] = {14, 9, 11, 7};

    printf("Aprobados: %d de %d\n", count_passing(grades, MAX_STUDENTS), MAX_STUDENTS);
    return 0;
}
image_demo.cDescargar image_demo.c
// El módulo de imágenes en acción: copias de struct y una fila pasada como arreglo.
#include <stdio.h>

#include "lab/image.h"

void paint_copy(struct image img) {
    img.pixels[0][0] = 99;
    printf("En paint_copy, el primer píxel vale %d\n", img.pixels[0][0]);
}

void paint_row(int row[], int width) {
    for (int x = 0; x < width; x++) {
        row[x] = 99;
    }
}

int main(void) {
    struct image img = image_load("lab/images/tiny.pgm");
    if (!image_is_valid(img)) {
        fprintf(stderr, "No se pudo leer la imagen\n");
        return 1;
    }

    printf("sizeof(struct image) = %zu bytes\n", sizeof(struct image));
    image_print(img);

    paint_copy(img);
    printf("Después de paint_copy:\n");
    image_print(img);

    paint_row(img.pixels[0], img.width);
    printf("Después de paint_row:\n");
    image_print(img);
    return 0;
}
show_sun.cDescargar show_sun.c
// Carga una imagen, la dibuja en la terminal y guarda una copia.
#include <stdio.h>

#include "lab/image.h"

int main(void) {
    struct image img = image_load("lab/images/sun.pgm");
    if (!image_is_valid(img)) {
        fprintf(stderr, "No se pudo leer lab/images/sun.pgm\n");
        return 1;
    }

    image_show(img);
    if (!image_save(img, "build/sun_copy.pgm")) {
        fprintf(stderr, "No se pudo escribir build/sun_copy.pgm\n");
        return 1;
    }
    printf("Tamaño: %d x %d\n", img.width, img.height);
    return 0;
}
static_counter.cDescargar static_counter.c
// Una variable local nace en cada llamada. Una local static dura todo el programa.
#include <stdio.h>

int count_local(void) {
    int calls = 0;
    calls = calls + 1;
    return calls;
}

int count_static(void) {
    static int calls = 0;
    calls = calls + 1;
    return calls;
}

int main(void) {
    for (int i = 0; i < 3; i++) {
        int first = count_local();
        int second = count_static();
        printf("local: %d, static: %d\n", first, second);
    }
    return 0;
}
struct_copy.cDescargar struct_copy.c
// Un struct se copia completo, aunque tenga un arreglo adentro.
#include <stdio.h>

struct row {
    int size;
    int cells[3];
};

void change_copy(struct row r) {
    r.cells[0] = 99;
    printf("en change_copy: cells[0] = %d\n", r.cells[0]);
}

struct row changed(struct row r) {
    r.cells[0] = 99;
    return r;
}

void change_cells(int cells[]) {
    cells[1] = 77;
}

int main(void) {
    struct row a = {3, {1, 2, 3}};

    change_copy(a);
    printf("en main: cells[0] = %d\n", a.cells[0]);

    struct row b = changed(a);
    printf("a: %d, b: %d\n", a.cells[0], b.cells[0]);

    change_cells(a.cells);
    printf("a.cells[1] = %d\n", a.cells[1]);
    return 0;
}

Errores

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

errors/assign_constant.cDescargar errors/assign_constant.c
// No compila: una constante global no se puede cambiar.
#include <stdio.h>

const int PASSING_GRADE = 11;

void make_it_easier(void) {
    PASSING_GRADE = 5;
}

int main(void) {
    make_it_easier();
    printf("%d\n", PASSING_GRADE);
    return 0;
}
errors/caller_local.cDescargar errors/caller_local.c
// No compila: una función no ve las locales de quien la llama.
#include <stdio.h>

void show_total(void) {
    printf("%d\n", total);
}

int main(void) {
    int total = 5;

    show_total();
    printf("%d\n", total);
    return 0;
}
errors/const_size.cDescargar errors/const_size.c
// No compila: una variable const no sirve como tamaño de un campo de un struct.
#include <stdio.h>

const int MAX_WIDTH = 64;

struct line {
    int cells[MAX_WIDTH];
};

int main(void) {
    printf("%zu\n", sizeof(struct line));
    return 0;
}
errors/out_of_block.cDescargar errors/out_of_block.c
// No compila: square solo existe dentro del bloque del for.
#include <stdio.h>

int main(void) {
    for (int i = 1; i <= 3; i++) {
        int square = i * i;
        printf("%d\n", square);
    }
    printf("%d\n", square);
    return 0;
}
errors/sizeof_param.cDescargar errors/sizeof_param.c
// Compila con advertencia: dentro de la función, sizeof no mide el arreglo.
#include <stdio.h>

void show_size(int values[]) {
    printf("en la función: %zu\n", sizeof(values));
}

int main(void) {
    int data[5] = {1, 2, 3, 4, 5};

    printf("en main: %zu\n", sizeof(data));
    show_size(data);
    return 0;
}
errors/static_init.cDescargar errors/static_init.c
// No compila: una local static se inicia antes de correr el programa, sin saber n.
#include <stdio.h>

int next_from(int n) {
    static int value = n;
    value = value + 1;
    return value;
}

int main(void) {
    printf("%d\n", next_from(10));
    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 el módulo de imágenes."""
import pathlib
import subprocess
import tempfile
import unittest

HERE = pathlib.Path(__file__).parent
BUILD = HERE / "build"
FLAGS = ["-Wall", "-Wextra", "-std=c17", "-g", "-fsanitize=address,undefined"]
WITH_MODULE = {"image_demo", "show_sun"}
PLAIN = ["frames", "blocks", "static_counter", "global_constant", "by_value",
         "array_param", "struct_copy"]


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


def run_driver(body, files=None):
    """Compila un main propio con el módulo y lo corre 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")
        source = folder / "driver.c"
        source.write_text(
            '#include <stdio.h>\n#include "image.h"\nint main(void) {\n' + body + "\nreturn 0;\n}\n",
            encoding="utf-8",
        )
        binary = folder / "driver"
        subprocess.run(
            ["gcc", *FLAGS, "-Werror", f"-I{HERE / 'lab'}", str(source),
             str(HERE / "lab" / "image.c"), "-o", str(binary)],
            check=True, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
        )
        done = subprocess.run(
            [str(binary)], cwd=folder, text=True, encoding="utf-8",
            stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=20,
        )
        return done.stdout, done.returncode


def load_status(content):
    body = 'struct image img = image_load("in.pgm"); printf("%d\\n", image_is_valid(img));'
    out, code = run_driver(body, {"in.pgm": content})
    return out.strip(), code


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

    def test_frames_keep_callers_variables(self):
        self.assertEqual(run("frames")[0].strip(), "a = 2, b = 5, c = 17")

    def test_static_survives_and_local_does_not(self):
        lines = run("static_counter")[0].splitlines()
        self.assertEqual(lines, ["local: 1, static: 1", "local: 1, static: 2", "local: 1, static: 3"])

    def test_struct_is_copied_but_array_is_not(self):
        out = run("struct_copy")[0]
        self.assertIn("en main: cells[0] = 1", out)
        self.assertIn("a.cells[1] = 77", out)

    def test_lab_frame_programs_run(self):
        for name in ("frames_calls", "frames_static", "frames_array"):
            with self.subTest(program=name), tempfile.TemporaryDirectory() as folder:
                binary = pathlib.Path(folder) / name
                subprocess.run(
                    ["gcc", *FLAGS, "-Werror", str(HERE / "lab" / f"{name}.c"), "-o", str(binary)],
                    check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                )
                done = subprocess.run([str(binary)], stdout=subprocess.PIPE, timeout=10)
                self.assertEqual(done.returncode, 0)


class ImageModule(unittest.TestCase):
    def test_reads_comments_anywhere_between_numbers(self):
        out, code = run_driver(
            'image_print(image_load("in.pgm"));',
            {"in.pgm": "P2\n# uno\n2 # dos\n1\n# tres\n9\n3#cuatro\n4\n"},
        )
        self.assertEqual(code, 0, out)
        self.assertEqual(out.split(), ["2", "x", "1,", "max", "9", "3", "4"])

    def test_good_file_is_valid(self):
        self.assertEqual(load_status("P2\n1 1\n255\n7\n"), ("1", 0))

    def test_largest_size_is_valid(self):
        pixels = " ".join(["1"] * (64 * 64))
        self.assertEqual(load_status(f"P2\n64 64\n255\n{pixels}\n"), ("1", 0))

    def test_bad_files_give_an_invalid_image(self):
        bad = {
            "formato binario": "P5\n1 1\n255\n7\n",
            "vacío": "",
            "ancho cero": "P2\n0 1\n255\n",
            "demasiado ancho": "P2\n65 1\n255\n" + "0 " * 65,
            "demasiado alto": "P2\n1 65\n255\n" + "0 " * 65,
            "max_value cero": "P2\n1 1\n0\n0\n",
            "max_value grande": "P2\n1 1\n256\n0\n",
            "faltan píxeles": "P2\n2 2\n255\n1 2 3\n",
            "píxel mayor que max": "P2\n1 1\n100\n101\n",
            "píxel negativo": "P2\n1 1\n255\n-1\n",
            "letra": "P2\n1 1\n255\nabc\n",
            "número enorme": "P2\n99999999999999999999 1\n255\n0\n",
        }
        for why, content in bad.items():
            with self.subTest(why=why):
                self.assertEqual(load_status(content), ("0", 0))

    def test_missing_file_gives_an_invalid_image(self):
        out, code = run_driver('printf("%d\\n", image_is_valid(image_load("nada.pgm")));')
        self.assertEqual((out.strip(), code), ("0", 0))

    def test_save_and_load_give_the_same_image(self):
        body = """
        int values[64 * 3];
        for (int i = 0; i < 64 * 3; i++) { values[i] = i % 256; }
        struct image img = image_from_values(64, 3, 255, values);
        printf("%d\\n", image_save(img, "out.pgm"));
        printf("%d\\n", image_equal(image_load("out.pgm"), img));
        """
        out, code = run_driver(body)
        self.assertEqual((out.split(), code), (["1", "1"], 0))

    def test_saved_lines_are_at_most_70_characters(self):
        body = """
        struct image img = image_new(64, 2, 255, 255);
        printf("%d\\n", image_save(img, "out.pgm"));
        FILE *file = fopen("out.pgm", "r");
        if (file == NULL) { return 1; }
        char line[200];
        int longest = 0;
        while (fgets(line, sizeof(line), file) != NULL) {
            int length = 0;
            while (line[length] != '\\0') { length++; }
            if (length > longest) { longest = length; }
        }
        fclose(file);
        printf("%d\\n", longest <= 71);
        """
        out, code = run_driver(body)
        self.assertEqual((out.split(), code), (["1", "1"], 0))

    def test_new_checks_its_arguments(self):
        body = """
        printf("%d", image_is_valid(image_new(1, 1, 255, 0)));
        printf("%d", image_is_valid(image_new(0, 1, 255, 0)));
        printf("%d", image_is_valid(image_new(1, 65, 255, 0)));
        printf("%d", image_is_valid(image_new(1, 1, 256, 0)));
        printf("%d", image_is_valid(image_new(1, 1, 255, 256)));
        printf("%d", image_is_valid(image_new(1, 1, 255, -1)));
        """
        out, code = run_driver(body)
        self.assertEqual((out.strip(), code), ("100000", 0))

    def test_equal_ignores_unused_cells(self):
        body = """
        struct image a = image_new(2, 2, 255, 7);
        struct image b = a;
        b.pixels[5][5] = 1;
        printf("%d", image_equal(a, b));
        b.pixels[1][1] = 1;
        printf("%d", image_equal(a, b));
        """
        out, code = run_driver(body)
        self.assertEqual((out.strip(), code), ("10", 0))


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 filters.c de esa carpeta y con el módulo de
lab/, sin correr las pruebas de ustedes.
"""
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 compile_and_run(main_source, sources):
    with tempfile.TemporaryDirectory() as name:
        folder = pathlib.Path(name)
        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",
            stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=30,
        )
        return done.stdout, done.returncode


def apply(call, width, height, max_value, values):
    """Devuelve (ancho, alto, max, píxeles) de lo que da call sobre la imagen img."""
    numbers = ", ".join(map(str, values))
    source = (
        '#include "filters.h"\nint main(void) {\n'
        f"    const int values[] = {{{numbers}}};\n"
        f"    struct image img = image_from_values({width}, {height}, {max_value}, values);\n"
        f"    image_print({call});\n    return 0;\n}}\n"
    )
    out, code = compile_and_run(source, [SOURCE / "filters.c", HERE / "lab" / "image.c"])
    if code != 0:
        raise AssertionError(f"El programa falló:\n{out}")
    numbers = [int(n) for n in re.findall(r"\d+", out)]
    return numbers[0], numbers[1], numbers[2], numbers[3:]


def reference_blur(values, width, height, radius):
    result = []
    for y in range(height):
        for x in range(width):
            window = [values[ny * width + nx]
                      for ny in range(max(0, y - radius), min(height, y + radius + 1))
                      for nx in range(max(0, x - radius), min(width, x + radius + 1))]
            result.append(sum(window) // len(window))
    return result


@unittest.skipUnless((SOURCE / "filters.c").exists(), f"no está {LAB_DIR}/filters.c")
class Filters(unittest.TestCase):
    def test_invert(self):
        values = [0, 10, 255, 30, 40, 100]
        self.assertEqual(apply("invert(img)", 3, 2, 255, values),
                         (3, 2, 255, [255, 245, 0, 225, 215, 155]))

    def test_invert_uses_the_max_value_of_the_image(self):
        self.assertEqual(apply("invert(img)", 2, 1, 15, [0, 5]), (2, 1, 15, [15, 10]))

    def test_threshold_with_the_exact_level(self):
        self.assertEqual(apply("threshold(img, 100)", 3, 2, 255, [99, 100, 101, 0, 255, 7]),
                         (3, 2, 255, [0, 255, 255, 0, 255, 0]))

    def test_threshold_uses_the_max_value_of_the_image(self):
        self.assertEqual(apply("threshold(img, 8)", 2, 1, 15, [7, 8]), (2, 1, 15, [0, 15]))

    def test_flip_with_odd_width(self):
        self.assertEqual(apply("flip_horizontal(img)", 3, 2, 255, [1, 2, 3, 4, 5, 6]),
                         (3, 2, 255, [3, 2, 1, 6, 5, 4]))

    def test_flip_with_even_width(self):
        self.assertEqual(apply("flip_horizontal(img)", 4, 1, 255, [1, 2, 3, 4]),
                         (4, 1, 255, [4, 3, 2, 1]))

    def test_blur_center_and_borders(self):
        # La esquina que tiene el 90 separa las reglas de borde: 22 solo sale si se
        # promedian únicamente los píxeles que existen.
        values = [90, 0, 0, 0, 0, 0, 0, 0, 0]
        self.assertEqual(apply("box_blur(img, 1)", 3, 3, 255, values),
                         (3, 3, 255, [22, 15, 0, 15, 10, 0, 0, 0, 0]))

    def test_blur_reads_the_original_values(self):
        # Si se escribe sobre la misma imagen, los últimos píxeles usan valores ya promediados.
        values = [0, 0, 0, 0, 90, 0, 0, 0, 0]
        self.assertEqual(apply("box_blur(img, 1)", 3, 3, 255, values),
                         (3, 3, 255, [22, 15, 22, 15, 10, 15, 22, 15, 22]))

    def test_blur_of_a_uniform_image_does_not_darken_the_border(self):
        self.assertEqual(apply("box_blur(img, 1)", 3, 2, 255, [80] * 6)[3], [80] * 6)
        self.assertEqual(apply("box_blur(img, 1)", 3, 2, 255, [80, 80, 80, 80, 80, 86])[3],
                         reference_blur([80, 80, 80, 80, 80, 86], 3, 2, 1))

    def test_blur_with_other_radius_and_not_square(self):
        values = [3 * i % 200 for i in range(35)]
        for radius in (0, 2, 9):
            with self.subTest(radius=radius):
                self.assertEqual(apply(f"box_blur(img, {radius})", 7, 5, 255, values),
                                 (7, 5, 255, reference_blur(values, 7, 5, radius)))

    def test_filters_on_the_largest_image(self):
        values = [(x * 7 + y * 11) % 256 for y in range(64) for x in range(64)]
        out = apply("box_blur(flip_horizontal(invert(threshold(img, 128))), 1)", 64, 64, 255, values)
        cut = [255 if v >= 128 else 0 for v in values]
        inverted = [255 - v for v in cut]
        flipped = [inverted[y * 64 + 63 - x] for y in range(64) for x in range(64)]
        self.assertEqual(out[3], reference_blur(flipped, 64, 64, 1))


@unittest.skipUnless((SOURCE / "filter_tests.c").exists(), f"no está {LAB_DIR}/filter_tests.c")
class YourTests(unittest.TestCase):
    def source_without_comments(self):
        text = (SOURCE / "filter_tests.c").read_text(encoding="utf-8")
        return re.sub(r"//[^\n]*", "", text)

    def test_each_filter_has_tests(self):
        text = self.source_without_comments()
        for name in ("invert", "threshold", "flip_horizontal", "box_blur"):
            with self.subTest(filter=name):
                self.assertRegex(text, rf"\b{name}\(", f"filter_tests.c no prueba {name}")

    def test_your_tests_pass(self):
        source = (SOURCE / "filter_tests.c").read_text(encoding="utf-8")
        out, code = compile_and_run(source, [SOURCE / "filters.c", HERE / "lab" / "image.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-03.md"
SLIDES = ROOT / "slides" / "week-03.tex"
# Antes de un bloque cuya salida cambia con el sistema. No se compara.
SYSTEM_DEPENDENT = "<!-- depende del sistema -->"
# Antes de una sesión de gdb. Las direcciones y los números de proceso no se comparan.
GDB_SESSION = "<!-- sesión de gdb -->"
# Igual, pero con variables sin inicializar, cuyo valor tampoco se compara.
GDB_GARBAGE = "<!-- sesión de gdb, con basura -->"
# Antes de una parte de lo que escribe filter_demo con la solución.
DEMO_OUTPUT = "<!-- salida de filter_demo -->"
# 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"]


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",
        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 / "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 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, garbage):
    """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
        if garbage and re.fullmatch(r"\w+ = -?\d+", line):
            continue
        line = re.sub(r"0x[0-9a-f]+", "ADDR", line)
        line = re.sub(r"process \d+", "process N", 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.startswith("<!-- sesión de gdb"):
                continue
            for command, expected in split_commands(lines):
                actual = clean_lines(run_shell(command))
                checked += 1
                if 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 not in (GDB_SESSION, GDB_GARBAGE):
                continue
            program = lines[0].split()[-1]
            source = pathlib.Path(program).name.removesuffix("_gdb") + ".c"
            subprocess.run(["gcc", *GDB_FLAGS, source, "-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)
            garbage = mark == GDB_GARBAGE
            wanted = stable_gdb_lines(lines[1:], garbage)
            got = stable_gdb_lines(actual.splitlines(), garbage)
            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, 3)

    @unittest.skipUnless(NOTES.exists() and (HERE / "solution").exists(), "falta la solución")
    def test_demo_output_in_notes(self):
        actual = clean_lines(run_shell("build/solution/filter_demo"))
        for lines, mark in note_blocks("text"):
            if mark == DEMO_OUTPUT:
                text = "\n".join(line.rstrip() for line in lines)
                self.assertIn(text, "\n".join(actual))


class SlidesAreTrue(unittest.TestCase):
    @unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
    def test_terminal_blocks_in_slides(self):
        # Las sesiones de gdb 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/filter_demo.cDescargar lab/filter_demo.c
// Aplica cada filtro a una imagen, la dibuja y guarda los resultados en build/.
#include <stdio.h>

#include "filters.h"

static int show_and_save(const char title[], struct image img, const char name[]) {
    printf("%s\n", title);
    image_show(img);
    if (!image_save(img, name)) {
        fprintf(stderr, "No se pudo escribir %s\n", name);
        return 0;
    }
    return 1;
}

int main(void) {
    struct image img = image_load("lab/images/sun.pgm");
    if (!image_is_valid(img)) {
        fprintf(stderr, "No se pudo leer lab/images/sun.pgm\n");
        return 1;
    }

    int ok = show_and_save("Original:", img, "build/sun_original.pgm");
    ok = show_and_save("invert:", invert(img), "build/sun_invert.pgm") && ok;
    ok = show_and_save("threshold 100:", threshold(img, 100), "build/sun_threshold.pgm") && ok;
    ok = show_and_save("flip_horizontal:", flip_horizontal(img), "build/sun_flip.pgm") && ok;
    ok = show_and_save("box_blur 2:", box_blur(img, 2), "build/sun_blur.pgm") && ok;
    return ok ? 0 : 1;
}
lab/filter_tests.cDescargar lab/filter_tests.c
// Sesión 2: pruebas de los filtros sobre imágenes pequeñas.
// Agreguen al menos una función de prueba para invert, threshold, flip_horizontal y
// box_blur, y llámenla desde main. Calculen a mano el resultado esperado.
#include <assert.h>
#include <stdio.h>

#include "filters.h"

static void test_brighten(void) {
    const int values[] = {0, 10, 20,
                          30, 250, 255};
    const int expected[] = {5, 15, 25,
                            35, 255, 255};
    struct image img = image_from_values(3, 2, 255, values);
    struct image wanted = image_from_values(3, 2, 255, expected);

    assert(image_equal(brighten(img, 5), wanted));
    assert(image_equal(brighten(img, 0), img));
}

int main(void) {
    test_brighten();

    printf("Todas las pruebas pasaron\n");
    return 0;
}
lab/filters.cDescargar lab/filters.c
// Sesión 2: escriban invert, threshold, flip_horizontal y box_blur.
// brighten ya está hecha y sirve de modelo. Lo que pide cada filtro está en filters.h.
#include "filters.h"

struct image brighten(struct image img, int amount) {
    for (int y = 0; y < img.height; y++) {
        for (int x = 0; x < img.width; x++) {
            int value = img.pixels[y][x] + amount;
            if (value < 0) {
                value = 0;
            }
            if (value > img.max_value) {
                value = img.max_value;
            }
            img.pixels[y][x] = value;
        }
    }
    return img;
}

struct image invert(struct image img) {
    // Escriban aquí su código.
    return img;
}

struct image threshold(struct image img, int level) {
    (void) level;  // borren esta línea cuando usen level
    // Escriban aquí su código.
    return img;
}

struct image flip_horizontal(struct image img) {
    // Escriban aquí su código.
    return img;
}

struct image box_blur(struct image img, int radius) {
    (void) radius;  // borren esta línea cuando usen radius
    // Escriban aquí su código.
    return img;
}
lab/filters.hDescargar lab/filters.h
// Filtros: reciben una copia de la imagen y devuelven la imagen nueva.
#ifndef FILTERS_H
#define FILTERS_H

#include "image.h"

// Suma amount a cada píxel, sin salirse de 0 a max_value. Ya está hecho: es el modelo.
struct image brighten(struct image img, int amount);

// Cada píxel p pasa a max_value - p.
struct image invert(struct image img);

// Cada píxel mayor o igual que level pasa a max_value. Los demás, a 0.
struct image threshold(struct image img, int level);

// Espejo izquierda-derecha: cada fila queda al revés.
struct image flip_horizontal(struct image img);

// Cada píxel pasa al promedio de la ventana cuadrada de lado 2 * radius + 1 que lo tiene
// al centro. radius no es negativo. En los bordes se promedian solo los píxeles de la
// ventana que están dentro de la imagen. El promedio se trunca con la división entera.
struct image box_blur(struct image img, int radius);

#endif
lab/frames_array.cDescargar lab/frames_array.c
// Sesión 1, programa 3. Dibujen los marcos en cada punto que pide lab/README.md.
#include <stdio.h>

struct pair {
    int left;
    int right;
};

struct pair swapped(struct pair p) {
    int old_left = p.left;
    p.left = p.right;
    p.right = old_left;
    return p;
}

void double_all(int values[], int n) {
    for (int i = 0; i < n; i++) {
        values[i] = values[i] * 2;
    }
    n = -1;
    printf("n = %d\n", n);
}

int main(void) {
    int data[3] = {4, 5, 6};
    int n = 3;
    struct pair a = {1, 2};

    double_all(data, n);
    struct pair b = swapped(a);

    printf("%d %d %d n=%d\n", data[0], data[1], data[2], n);
    printf("a=(%d,%d) b=(%d,%d)\n", a.left, a.right, b.left, b.right);
    return 0;
}
lab/frames_calls.cDescargar lab/frames_calls.c
// Sesión 1, programa 1. Dibujen los marcos en cada punto que pide lab/README.md.
#include <stdio.h>

int clamp(int value, int high) {
    if (value > high) {
        return high;
    }
    return value;
}

int final_grade(int grade, int bonus) {
    int total = grade + bonus;
    int capped = clamp(total, 20);
    return capped;
}

int main(void) {
    int grade = 18;
    int bonus = 3;
    int total = final_grade(grade, bonus);
    int other = final_grade(10, bonus);

    printf("%d %d %d %d\n", grade, bonus, total, other);
    return 0;
}
lab/frames_static.cDescargar lab/frames_static.c
// Sesión 1, programa 2. Dibujen los marcos en cada punto que pide lab/README.md.
#include <stdio.h>

const int STEP = 5;

int next_ticket(void) {
    static int last = 100;
    int before = last;
    last = last + STEP;
    return before;
}

int main(void) {
    int last = 0;

    for (int i = 0; i < 3; i++) {
        int last = next_ticket();
        printf("ticket %d\n", last);
    }
    printf("last = %d\n", last);
    return 0;
}
lab/image.cDescargar lab/image.c
// Implementación del módulo de imágenes. Ver image.h.
#include "image.h"

#include <ctype.h>
#include <stdio.h>

// Ningún dato válido de un PGM de este curso pasa de aquí.
#define NUMBER_LIMIT 100000

// En cada línea del archivo: el formato pide no pasar de 70 caracteres.
#define VALUES_PER_LINE 16

static struct image image_invalid(void) {
    struct image img = {0};
    return img;
}

static int valid_size(int width, int height, int max_value) {
    return width >= 1 && width <= IMAGE_MAX_WIDTH
        && height >= 1 && height <= IMAGE_MAX_HEIGHT
        && max_value >= 1 && max_value <= IMAGE_MAX_VALUE;
}

struct image image_new(int width, int height, int max_value, int value) {
    struct image img = image_invalid();
    if (!valid_size(width, height, max_value) || value < 0 || value > max_value) {
        return img;
    }

    img.width = width;
    img.height = height;
    img.max_value = max_value;
    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            img.pixels[y][x] = value;
        }
    }
    return img;
}

struct image image_from_values(int width, int height, int max_value, const int values[]) {
    struct image img = image_new(width, height, max_value, 0);
    if (!image_is_valid(img)) {
        return img;
    }

    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            int value = values[y * width + x];
            if (value < 0 || value > max_value) {
                return image_invalid();
            }
            img.pixels[y][x] = value;
        }
    }
    return img;
}

int image_is_valid(struct image img) {
    return img.width > 0;
}

int image_equal(struct image a, struct image b) {
    if (a.width != b.width || a.height != b.height || a.max_value != b.max_value) {
        return 0;
    }

    for (int y = 0; y < a.height; y++) {
        for (int x = 0; x < a.width; x++) {
            if (a.pixels[y][x] != b.pixels[y][x]) {
                return 0;
            }
        }
    }
    return 1;
}

// Devuelve el siguiente número del archivo, o -1 si lo que sigue no es un número.
// Se salta los espacios y los comentarios, que van de # hasta el fin de la línea.
static int read_number(FILE *file) {
    int c = fgetc(file);
    while (c == '#' || isspace(c)) {
        if (c == '#') {
            while (c != '\n' && c != EOF) {
                c = fgetc(file);
            }
        } else {
            c = fgetc(file);
        }
    }

    if (!isdigit(c)) {
        return -1;
    }

    int value = 0;
    while (isdigit(c)) {
        value = value * 10 + (c - '0');
        if (value > NUMBER_LIMIT) {
            return -1;
        }
        c = fgetc(file);
    }
    // Lo que no era parte del número le toca a la siguiente lectura.
    if (c != EOF) {
        ungetc(c, file);
    }
    return value;
}

// Lee la imagen de un archivo ya abierto. Lo que venga después del último píxel se ignora.
static struct image read_image(FILE *file) {
    if (fgetc(file) != 'P' || fgetc(file) != '2') {
        return image_invalid();
    }

    int width = read_number(file);
    int height = read_number(file);
    int max_value = read_number(file);
    struct image img = image_new(width, height, max_value, 0);
    if (!image_is_valid(img)) {
        return img;
    }

    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            int value = read_number(file);
            if (value < 0 || value > max_value) {
                return image_invalid();
            }
            img.pixels[y][x] = value;
        }
    }
    return img;
}

struct image image_load(const char name[]) {
    FILE *file = fopen(name, "r");
    if (file == NULL) {
        return image_invalid();
    }

    struct image img = read_image(file);
    fclose(file);
    return img;
}

int image_save(struct image img, const char name[]) {
    if (!image_is_valid(img)) {
        return 0;
    }

    FILE *file = fopen(name, "w");
    if (file == NULL) {
        return 0;
    }

    int written = fprintf(file, "P2\n%d %d\n%d\n", img.width, img.height, img.max_value);
    for (int y = 0; y < img.height && written >= 0; y++) {
        for (int x = 0; x < img.width && written >= 0; x++) {
            int last_in_line = x == img.width - 1 || x % VALUES_PER_LINE == VALUES_PER_LINE - 1;
            written = fprintf(file, "%d%c", img.pixels[y][x], last_in_line ? '\n' : ' ');
        }
    }

    // Al cerrar se escribe lo que quedaba pendiente, y eso también puede fallar.
    if (fclose(file) != 0 || written < 0) {
        return 0;
    }
    return 1;
}

void image_print(struct image img) {
    printf("%d x %d, max %d\n", img.width, img.height, img.max_value);
    for (int y = 0; y < img.height; y++) {
        for (int x = 0; x < img.width; x++) {
            printf("%4d", img.pixels[y][x]);
        }
        printf("\n");
    }
}

void image_show(struct image img) {
    const char shades[] = " .:-=+*#%@";
    int last_shade = (int) sizeof(shades) - 2;

    for (int y = 0; y < img.height; y++) {
        for (int x = 0; x < img.width; x++) {
            putchar(shades[img.pixels[y][x] * last_shade / img.max_value]);
        }
        putchar('\n');
    }
}
lab/image.hDescargar lab/image.h
// Módulo de imágenes en escala de grises, en formato PGM de texto (P2).
// Este archivo dice qué ofrece el módulo; image.c dice cómo lo hace.
#ifndef IMAGE_H
#define IMAGE_H

#define IMAGE_MAX_WIDTH 64
#define IMAGE_MAX_HEIGHT 64
#define IMAGE_MAX_VALUE 255

// Los píxeles en uso son pixels[y][x], con 0 <= y < height y 0 <= x < width.
// Cada uno vale de 0 (negro) a max_value (blanco).
// Una imagen con width igual a 0 es inválida: así avisan los errores.
struct image {
    int width;
    int height;
    int max_value;
    int pixels[IMAGE_MAX_HEIGHT][IMAGE_MAX_WIDTH];
};

// Devuelve una imagen con todos sus píxeles en value, o una inválida si algún dato
// no está en su rango.
struct image image_new(int width, int height, int max_value, int value);

// Igual, pero toma los píxeles de values, fila por fila (width * height elementos).
struct image image_from_values(int width, int height, int max_value, const int values[]);

// Devuelve 1 si la imagen es válida y 0 si no.
int image_is_valid(struct image img);

// Devuelve 1 si tienen el mismo tamaño, el mismo max_value y los mismos píxeles.
int image_equal(struct image a, struct image b);

// Lee un archivo PGM de texto. Devuelve una imagen inválida si no se puede abrir, si
// no es P2, si no cabe o si sus datos están mal.
struct image image_load(const char name[]);

// Escribe la imagen en un archivo PGM de texto. Devuelve 1 si pudo y 0 si no.
int image_save(struct image img, const char name[]);

// Escribe el tamaño y los números de cada píxel, para imágenes pequeñas.
void image_print(struct image img);

// Dibuja la imagen en la terminal con letras: más claro, más lleno.
void image_show(struct image img);

#endif
lab/README.mdDescargar lab/README.md
# Laboratorio de la semana 3: marcos en `gdb` y filtros de imágenes

Los comandos se corren desde `code/week-03`. Las ideas están en los apuntes, en la sección
"El laboratorio, paso a paso".

| Archivo | Qué es | ¿Lo cambian? |
|---|---|---|
| `frames_calls.c`, `frames_static.c`, `frames_array.c` | Los tres programas de la sesión 1 | No |
| `image.h`, `image.c` | El módulo que carga y guarda imágenes | No |
| `filters.h` | Lo que debe hacer cada filtro | No |
| `filters.c` | Los filtros: `brighten` hecho, los demás por escribir | **Sí** |
| `filter_tests.c` | Sus pruebas con `assert` | **Sí** |
| `filter_demo.c` | Aplica los filtros a `images/sun.pgm` y dibuja el resultado | No |
| `images/` | `tiny.pgm`, de 3 x 2, y `sun.pgm`, de 32 x 16 | No |

```sh
make gdb        # compila los programas de la sesión 1 en build/gdb/, con -g -O0
make lab        # compila filter_tests y filter_demo en build/lab/
make test-lab   # comprueba sus filtros; falla hasta que terminen
```

## Sesión 1

### Parte 1: dibujar y comprobar marcos

Para cada programa: **primero dibujen en papel, sin ejecutar**. Después ábranlo en `gdb`
y comparen con `backtrace`, `frame` e `info locals`. Si el dibujo y `gdb` no coinciden,
averigüen por qué antes de seguir. Una variable que todavía no recibió valor se dibuja
con `?`, y `gdb` les mostrará algo cualquiera.

**`frames_calls.c`**: `gdb build/gdb/frames_calls`.

1. Escriban lo que creen que imprime.
2. Dibujen la pila en la primera parada de `break clamp`.
3. Dibujen la pila en la segunda parada. ¿Qué cambió y qué no? ¿El marco de `clamp` es el
   mismo de la primera parada?
4. ¿Por qué `main` y `final_grade` pueden tener cada una su propio `total`?

**`frames_static.c`**: `gdb build/gdb/frames_static`.

1. Escriban lo que creen que imprime.
2. Con `break 9`, dibujen el marco de `next_ticket` en cada una de las tres paradas.
   Pongan `last` y `STEP` fuera de la pila.
3. Con `break 18`, parados dentro del `for`, ¿cuántas variables `last` tiene `main`? ¿Cuál
   usa `printf`?
4. Para cada `last` de este programa y para `STEP`, llenen una tabla con su alcance y su
   tiempo de vida.

**`frames_array.c`**: `gdb build/gdb/frames_array`.

1. Escriban lo que creen que imprime.
2. Con `break 20`, dibujen los marcos cuando ya terminó el ciclo. El parámetro `values` se
   dibuja como una flecha. ¿A dónde apunta?
3. Con `print values` y `print values[0]@3`, comprueben su flecha. Con `up` y `print data`,
   miren el arreglo original.
4. Con `break 13`, dibujen los marcos justo antes del `return`. ¿Por qué `a` de `main` no
   cambió, si `p` sí?
5. ¿Por qué `double_all` puede cambiar `data`, pero no `n`?

### Parte 2: leer el módulo

Lean `image.h` completo y luego `image.c`. Respondan por escrito, sin ejecutar nada. Las
respuestas salen del código. Las preguntas 1 a 6 son para la sesión. Las demás, si no
alcanza el tiempo, quedan para antes de la sesión 2.

1. ¿Qué devuelve `image_load` si el archivo no existe? ¿Cómo lo detecta quien la llama?
2. `struct image` reserva 64 x 64 píxeles. En una imagen de 3 x 2, ¿cuáles de esos píxeles
   se usan? ¿Qué valor tienen los otros cuando la imagen sale de `image_new`?
3. En `read_number`, ¿qué comentarios se saltan? ¿Qué pasa si aparece una letra?
4. ¿Por qué `read_number` puede devolver -1 sin confundirse con un dato válido?
5. ¿Quién cierra el archivo en `image_load`, y por qué `read_image` puede tener varios
   `return` sin arriesgarse a dejarlo abierto?
6. Cuando `main` llama a `image_is_valid(img)`, ¿cuántos bytes se copian? Mídanlo con
   `print sizeof(img)` en `gdb`, y piensen qué pasa en `image_equal`.
7. ¿Para qué sirve `ungetc` en `read_number`? Piensen en `3#comentario`.
8. ¿Por qué `read_image` no revisa `width`, `height` y `max_value` por su cuenta?
9. ¿Por qué `image_save` revisa también el resultado de `fclose`?
10. ¿En `image_from_values`, por qué el píxel de la fila `y` y columna `x` es
    `values[y * width + x]`?

## Sesión 2

### Parte 3: `invert`, `threshold` y `flip_horizontal`

Escríbanlos en `filters.c`, según `filters.h`, y agreguen sus pruebas en `filter_tests.c`,
con el resultado esperado calculado a mano. `test_brighten` es el modelo.

```sh
make lab && build/lab/filter_tests
```

### Parte 4: `box_blur`

Escríbanlo en `filters.c`, también con pruebas. Los apuntes explican las reglas posibles
para los bordes y cuál usamos aquí. Después respondan por escrito: ¿qué regla habrían
preferido para una foto, y por qué?

Cuando pasen sus pruebas, corran `make test-lab`. Luego miren el resultado:

```sh
build/lab/filter_demo
```
lab/images/sun.pgmDescargar lab/images/sun.pgm
P2
# Un sol a la izquierda y un cerro que sube hacia la derecha.
32 16
255
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 40 40 40 40 40 40 255 40 40 40 40 40 40 40
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 40 40 255 255 255 255 255 255 255 255 255 40 40 40
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 40 255 255 255 255 255 255 255 255 255 255 255 40 40
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 255 255 255 255 255 255 255 255 255 255 255 255 255 40
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 40 255 255 255 255 255 255 255 255 255 255 255 40 40
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 40 40 255 255 255 255 255 255 255 255 255 40 40 40
40 40 40 40 40 40 40 40 40 40 40 40 40 40 130 130
40 40 40 40 40 40 40 40 255 40 40 40 40 40 40 40
40 40 40 40 40 40 40 40 40 130 130 130 130 130 130 130
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 40 40 130 130 130 130 130 130 130 130 130 130 130 130
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 130
130 130 130 130 130 130 130 130 130 130 130 130 130 130 130 130
40 40 40 40 40 40 40 40 40 40 130 130 130 130 130 130
130 130 130 130 130 130 130 130 130 130 130 130 130 130 130 130
40 40 40 40 40 130 130 130 130 130 130 130 130 130 130 130
130 130 130 130 130 130 130 130 130 130 130 130 130 130 130 130
130 130 130 130 130 130 130 130 130 130 130 130 130 130 130 130
130 130 130 130 130 130 130 130 130 130 130 130 130 130 130 130
130 130 130 130 130 130 130 130 130 130 130 130 130 130 130 130
130 130 130 130 130 130 130 130 130 130 130 130 130 130 130 130
130 130 130 130 130 130 130 130 130 130 130 130 130 130 130 130
130 130 130 130 130 130 130 130 130 130 130 130 130 130 130 130
lab/images/tiny.pgmDescargar lab/images/tiny.pgm
P2
# Tres columnas y dos filas, con un comentario.
3 2
255
0 10 20
30 40 50

Compilación

Cómo se compilan y ejecutan los ejemplos.

MakefileDescargar Makefile
# Semana 3: el modelo de entornos e imágenes PGM. Se usa dentro del entorno del curso.
#
#   make           compila los programas de la clase en build/
#   make gdb       compila los programas de frames para gdb en build/gdb/ (-g -O0, sin sanitizers)
#   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/filters.c y lab/filter_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.

CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -g -fsanitize=address,undefined
GDBFLAGS = -Wall -Wextra -std=c17 -g -O0
BUILD = build
PROGRAMS = frames blocks static_counter global_constant by_value array_param struct_copy
FRAMES = frames_calls frames_static frames_array
MODULE = lab/image.c lab/image.h
LAB_PROGRAMS = filter_tests filter_demo

.PHONY: all gdb lab test test-lab clean

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

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

$(BUILD)/image_demo: image_demo.c $(MODULE) | $(BUILD)
	$(CC) $(CFLAGS) image_demo.c lab/image.c -o $@

$(BUILD)/show_sun: show_sun.c $(MODULE) | $(BUILD)
	$(CC) $(CFLAGS) show_sun.c lab/image.c -o $@

$(BUILD)/gdb/%: lab/%.c | $(BUILD)/gdb
	$(CC) $(GDBFLAGS) $< -o $@

$(BUILD)/lab/filter_tests: lab/filter_tests.c lab/filters.c lab/filters.h $(MODULE) | $(BUILD)/lab
	$(CC) $(CFLAGS) -Ilab lab/filter_tests.c lab/filters.c lab/image.c -o $@

$(BUILD)/lab/filter_demo: lab/filter_demo.c lab/filters.c lab/filters.h $(MODULE) | $(BUILD)/lab
	$(CC) $(CFLAGS) -Ilab lab/filter_demo.c lab/filters.c lab/image.c -o $@

$(BUILD)/solution/filter_tests: solution/filter_tests.c solution/filters.c lab/filters.h $(MODULE) | $(BUILD)/solution
	$(CC) $(CFLAGS) -Ilab solution/filter_tests.c solution/filters.c lab/image.c -o $@

$(BUILD)/solution/filter_demo: lab/filter_demo.c solution/filters.c lab/filters.h $(MODULE) | $(BUILD)/solution
	$(CC) $(CFLAGS) -Ilab lab/filter_demo.c solution/filters.c lab/image.c -o $@

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

gdb: $(FRAMES:%=$(BUILD)/gdb/%)

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

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