Semana 15 — El intérprete

Objetivos

  • Evaluar programas completos del lenguaje Mini, con variables y funciones.
  • Explicar las llamadas del lenguaje interpretado con el modelo de entornos de la semana 3.
  • Probar un programa grande con pruebas de varios niveles.

Teoría — El intérprete: variables, funciones y marcos · Repaso

Lectura: Apuntes, semana 15.

Temas:

  • Agregar instrucciones al árbol: let, print, if, def y llamadas.
  • Evaluar una llamada: un marco nuevo con sus parámetros. El modelo de entornos de la semana 3, ahora escrito por ustedes.
  • Por qué funciona la recursión en Mini: la recursión del intérprete sostiene la del lenguaje.
  • Tabla de funciones y errores de ejecución: nombre desconocido y número de argumentos incorrecto.
  • Repaso del curso: qué idea sirvió en cada parte del intérprete.

Laboratorio — Terminar y probar el intérprete

Sesión 1:

  1. Unir el parser y el evaluador, y correr programas de Mini desde un archivo.
  2. Agregar let, print e if.
  3. Agregar def y llamadas a funciones usando la tabla Env dada: cada llamada crea un marco cuyo padre es el marco global.

Sesión 2: 4. Correr el conjunto de pruebas: factorial y Fibonacci en Mini, errores esperados y comprobación de que no haya fugas. 5. Escribir un programa propio en Mini y una prueba que encuentre un bug en el intérprete. 6. Repaso para el examen final.

Preguntas de repaso

  1. En def f(n) = if n < 2 then 1 else n * f(n - 1), ¿cuántos marcos de f existen en el momento más profundo de f(3), además del marco global?
  2. ¿Qué pasa en el intérprete de C si un programa de Mini tiene una recursión sin fin?
  3. ¿Qué pasa si un parámetro se llama igual que una variable global? ¿Qué valor ve el cuerpo de la función, y qué valor tiene la global al volver?
  4. ¿Qué parte del curso usaron en el analizador léxico, en el parser y en el evaluador?

Código de la semana

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

Ejemplos (1)Pruebas (3)Laboratorio (35)Compilación (1)

Ejemplos

Los programas que usan los apuntes y la clase.

frames.cDescargar frames.c
// Lo que hace una llamada de Mini con los marcos, escrito a mano con Env.
//
//     let rate = 2
//     def scale(n) = n * rate
//     def outer(n) = scale(n + 1)
//     print outer(4)
#include <stdio.h>

#include "env.h"

// Busca name desde ese marco y escribe lo que encuentra.
static void show(const char *where, const Env *frame, const char *name) {
    double value;
    if (env_get(frame, name, &value)) {
        printf("%-10s %-5s = %g\n", where, name, value);
    } else {
        printf("%-10s %-5s no existe\n", where, name);
    }
}

int main(void) {
    Env *global = env_new(NULL);
    Env *outer = env_new(global);
    Env *scale = env_new(global);       // cuelga del global, no de outer
    Env *wrong = env_new(outer);        // lo que NO hace Mini: colgar del marco de quien llama
    if (global == NULL || outer == NULL || scale == NULL || wrong == NULL) {
        fprintf(stderr, "no hay memoria\n");
        env_free(wrong);
        env_free(scale);
        env_free(outer);
        env_free(global);
        return 1;
    }
    if (!env_set(global, "rate", 2) || !env_set(outer, "n", 4) || !env_set(scale, "n", 5)) {
        fprintf(stderr, "no hay memoria\n");
        env_free(wrong);
        env_free(scale);
        env_free(outer);
        env_free(global);
        return 1;
    }

    show("global", global, "rate");
    show("global", global, "n");
    show("outer", outer, "n");
    show("outer", outer, "rate");
    show("scale", scale, "n");
    show("scale", scale, "rate");
    show("wrong", wrong, "n");

    // Primero los marcos hijos, aunque Env no libera al padre: el orden de la pila.
    env_free(wrong);
    env_free(scale);
    env_free(outer);
    env_free(global);
    return 0;
}

Pruebas

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

test_examples.pyDescargar test_examples.py
"""Comprueba el ejemplo de clase, los módulos ya terminados y lo que los apuntes dicen del intérprete."""
import pathlib
import shutil
import subprocess
import tempfile
import unittest

HERE = pathlib.Path(__file__).parent
BUILD = HERE / "build"
LAB = HERE / "lab"
SOLUTION = HERE / "solution"
BASE = ["-Wall", "-Wextra", "-std=c17"]
FLAGS = [*BASE, "-g", "-fsanitize=address,undefined"]
FINISHED = ["env.c", "expr.c", "lexer.c", "stmt.c"]
REPORTS = ("AddressSanitizer", "LeakSanitizer", "runtime error")


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


def run_c(body, sources=FINISHED):
    """Compila y corre un main con los módulos dados, y devuelve (salida, código)."""
    with tempfile.TemporaryDirectory() as name:
        folder = pathlib.Path(name)
        driver = folder / "driver.c"
        driver.write_text(body, encoding="utf-8")
        binary = folder / "driver"
        files = [str(driver)] + [str(LAB / s) for s in sources]
        done = subprocess.run(["gcc", *FLAGS, "-Werror", f"-I{LAB}", *files, "-o", str(binary)],
                              text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        if done.returncode != 0:
            raise AssertionError(f"No compila:\n{done.stdout}")
        return run(binary)


def solution_copy(folder, change=None):
    """Copia lab/ con la solución encima. change recibe el texto de interp.c y lo modifica."""
    for path in list(LAB.glob("*.[ch]")) + list(SOLUTION.glob("*.c")):
        shutil.copy(path, folder)
    if change is not None:
        interp = pathlib.Path(folder) / "interp.c"
        interp.write_text(change(interp.read_text(encoding="utf-8")), encoding="utf-8")


def compile_all(folder, flags, name):
    binary = pathlib.Path(folder) / name
    sources = [str(path) for path in pathlib.Path(folder).glob("*.c")]
    subprocess.run(["gcc", *flags, *sources, "-o", str(binary)], check=True,
                   stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    return binary


class Frames(unittest.TestCase):
    def test_frames_program(self):
        out, code = run(BUILD / "frames")
        self.assertEqual(code, 0, out)
        self.assertIn("scale      rate  = 2", out)
        self.assertIn("global     n     no existe", out)
        self.assertIn("wrong      n     = 4", out)


class Compiled(unittest.TestCase):
    def test_lab_compiles_without_warnings(self):
        for path in sorted(LAB.glob("*.c")):
            with self.subTest(file=path.name):
                subprocess.run(["gcc", *FLAGS, "-Werror", f"-I{LAB}", "-c", str(path), "-o", "/dev/null"],
                               check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

    def test_finished_modules_also_compile_with_o2(self):
        for name in FINISHED + ["mini.c"]:
            with self.subTest(file=name):
                subprocess.run(["gcc", *BASE, "-O2", "-Werror", f"-I{LAB}", "-c", str(LAB / name), "-o", "/dev/null"],
                               check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

    def test_env_is_a_copy_of_the_contract(self):
        # Env se usa solo por su encabezado: nadie entra a sus campos.
        for name in ("parser.c", "interp.c", "expr.c", "mini.c"):
            self.assertNotIn("->first", (LAB / name).read_text(encoding="utf-8"), name)


class FinishedExpr(unittest.TestCase):
    def test_print_keeps_the_tree(self):
        body = r"""
        #include <stdio.h>
        #include "expr.h"
        static Expr *n(double v) { return expr_number(v); }
        static Expr *v(const char *s) { return expr_variable(s); }
        static void show(Expr *e) { expr_print(e, stdout); printf("\n"); expr_free(e); }
        int main(void) {
            show(expr_binary(OP_SUB, n(1), expr_binary(OP_SUB, n(2), n(3))));
            show(expr_binary(OP_SUB, expr_binary(OP_SUB, n(1), n(2)), n(3)));
            show(expr_binary(OP_MUL, expr_binary(OP_ADD, v("a"), n(1)), v("b")));
            show(expr_binary(OP_ADD, v("a"), expr_binary(OP_MUL, n(1), v("b"))));
            show(expr_binary(OP_LESS, expr_binary(OP_LESS, n(1), n(2)), n(3)));
            show(expr_binary(OP_ADD, expr_if(v("c"), n(1), n(2)), n(3)));
            show(expr_if(expr_if(v("a"), n(1), n(0)), n(2), expr_if(v("b"), n(3), n(4))));
            show(expr_call("f", NULL, 0));
            return 0;
        }
        """
        out, code = run_c(body)
        self.assertEqual(code, 0, out)
        self.assertEqual(out.splitlines(), [
            "1 - (2 - 3)", "1 - 2 - 3", "(a + 1) * b", "a + 1 * b", "(1 < 2) < 3", "(if c then 1 else 2) + 3",
            "if if a then 1 else 0 then 2 else if b then 3 else 4", "f()",
        ])

    def test_a_failed_eval_does_not_touch_the_result(self):
        body = r"""
        #include <stdio.h>
        #include "expr.h"
        int main(void) {
            Env *env = env_new(NULL);
            if (env == NULL) { return 1; }
            Expr *trees[3] = {
                expr_variable("missing"),
                expr_binary(OP_DIV, expr_number(1), expr_number(0)),
                expr_if(expr_number(1), expr_call("f", NULL, 0), expr_number(2)),
            };
            for (int i = 0; i < 3; i++) {
                double r = 42;
                int ok = expr_eval(trees[i], env, &r);
                printf("%d %g\n", ok, r);
                expr_free(trees[i]);
            }
            env_free(env);
            return 0;
        }
        """
        out, code = run_c(body, ["env.c", "expr.c"])
        self.assertEqual(code, 0, out)
        self.assertEqual(out.splitlines(), ["0 42", "0 42", "0 42"])

    def test_simplify_follows_the_rules_of_week_13(self):
        body = r"""
        #include <stdio.h>
        #include "expr.h"
        int main(void) {
            // (0 - 1) * 0 es 0, no -0: la regla de x * 0 va antes que calcular.
            Expr *e = expr_simplify(expr_binary(OP_MUL, expr_binary(OP_SUB, expr_number(0), expr_number(1)), expr_number(0)));
            expr_print(e, stdout);
            printf("\n%d\n", expr_simplify(NULL) == NULL);
            expr_free(e);
            return 0;
        }
        """
        out, code = run_c(body, ["env.c", "expr.c"])
        self.assertEqual(code, 0, out)
        self.assertEqual(out.splitlines(), ["0", "1"])

    def test_constructors_take_ownership_when_they_fail(self):
        body = r"""
        #include <stdio.h>
        #include <stdlib.h>
        #include "expr.h"
        int main(void) {
            printf("%d", expr_binary(OP_ADD, expr_variable("a"), NULL) == NULL);
            printf("%d", expr_binary(OP_ADD, NULL, expr_variable("a")) == NULL);
            printf("%d", expr_if(expr_variable("a"), NULL, expr_number(1)) == NULL);
            Expr **args = malloc(2 * sizeof *args);
            if (args == NULL) { return 1; }
            args[0] = expr_number(1);
            args[1] = NULL;
            printf("%d\n", expr_call("f", args, 2) == NULL);
            return 0;
        }
        """
        out, code = run_c(body)
        self.assertEqual((out.strip(), code), ("1111", 0), out)

    def test_copy_equal_simplify_derive_eval(self):
        body = r"""
        #include <stdio.h>
        #include <stdlib.h>
        #include "expr.h"
        static Expr *n(double v) { return expr_number(v); }
        static Expr *v(const char *s) { return expr_variable(s); }
        static void show(Expr *e) { expr_print(e, stdout); printf("\n"); expr_free(e); }
        int main(void) {
            Expr **args = malloc(2 * sizeof *args);
            if (args == NULL) { return 1; }
            args[0] = expr_binary(OP_ADD, v("x"), n(0));
            args[1] = expr_if(v("a"), n(1), n(2));
            Expr *call = expr_call("f", args, 2);
            Expr *copy = expr_copy(call);
            printf("%d\n", expr_equal(call, copy));
            expr_free(copy);
            show(expr_simplify(call));
            show(expr_simplify(expr_binary(OP_MUL, expr_binary(OP_ADD, v("x"), n(0)), n(1))));
            show(expr_simplify(expr_binary(OP_MUL, v("x"), n(0))));
            show(expr_simplify(expr_binary(OP_ADD, n(2), expr_binary(OP_MUL, n(3), n(4)))));
            show(expr_simplify(expr_binary(OP_DIV, n(1), n(0))));
            Expr *e = expr_binary(OP_MUL, v("x"), v("x"));
            show(expr_simplify(expr_derive(e, "x")));
            expr_free(e);
            e = expr_binary(OP_LESS, v("x"), n(1));
            printf("%d\n", expr_derive(e, "x") == NULL);
            Env *env = env_new(NULL);
            double r = 0;
            if (env == NULL || !env_set(env, "x", 3)) { return 1; }
            int ok = expr_eval(e, env, &r);
            printf("%d %g\n", ok, r);
            expr_free(e);
            e = expr_if(v("x"), n(7), expr_binary(OP_DIV, n(1), n(0)));
            ok = expr_eval(e, env, &r);
            printf("%d %g\n", ok, r);
            expr_free(e);
            e = expr_call("f", NULL, 0);
            ok = expr_eval(e, env, &r);
            printf("%d\n", ok);
            expr_free(e);
            env_free(env);
            return 0;
        }
        """
        out, code = run_c(body)
        self.assertEqual(code, 0, out)
        self.assertEqual(out.splitlines(), [
            "1", "f(x, if a then 1 else 2)", "x", "0", "14", "1 / 0", "x + x", "1", "1 0", "1 7", "0",
        ])


class FinishedLexer(unittest.TestCase):
    def tokens(self, text):
        literal = text.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
        body = r"""
        #include <stdio.h>
        #include "lexer.h"
        int main(void) {
            Lexer *lexer = lexer_new("%s");
            if (lexer == NULL) { return 1; }
            Token t;
            do {
                t = lexer_next(lexer);
                printf("%%d:%%d:%%d:%%s\n", t.kind, t.line, t.column, t.text);
            } while (t.kind != TOKEN_END && t.kind != TOKEN_ERROR);
            lexer_free(lexer);
            return 0;
        }
        """ % literal
        out, code = run_c(body, ["lexer.c"])
        self.assertEqual(code, 0, out)
        return [line.split(":", 3) for line in out.splitlines()]

    def test_positions_and_comments(self):
        got = self.tokens("let x1 = 3.5 # nada\n  print <= != ==")
        self.assertEqual([t[3] for t in got], ["let", "x1", "=", "3.5", "print", "<=", "!=", "==", ""])
        self.assertEqual(got[4][1:3], ["2", "3"])
        self.assertEqual(got[0][0], "2")    # TOKEN_LET
        self.assertEqual(got[1][0], "1")    # TOKEN_NAME

    def test_errors(self):
        self.assertIn("inesperado", self.tokens("1 @")[-1][3])
        self.assertIn("!", self.tokens("!")[-1][3])
        self.assertIn("largo", self.tokens("a" * 64)[-1][3])
        self.assertEqual(self.tokens("a" * 63)[0][3], "a" * 63)
        self.assertIn("largo", self.tokens("1" * 70)[-1][3])

    def test_a_point_needs_digits(self):
        self.assertIn("inesperado", self.tokens("3.")[-1][3])
        self.assertEqual(self.tokens("3.25")[0][3], "3.25")


class FinishedExpressionParser(unittest.TestCase):
    """parse_expression, sin if ni llamadas: esa parte está en lab/parser.c antes de que la toquen."""

    def show(self, text):
        literal = text.replace('"', '\\"')
        body = r"""
        #include <stdio.h>
        #include "parser.h"
        int main(void) {
            ParseError error;
            Expr *e = parse_expression("%s", &error);
            if (e == NULL) {
                printf("%%d:%%d\n", error.line, error.column);
                return 0;
            }
            expr_print(e, stdout);
            printf("\n");
            expr_free(e);
            return 0;
        }
        """ % literal
        out, code = run_c(body, FINISHED + ["parser.c"])
        self.assertEqual(code, 0, out)
        return out.strip()

    def test_precedence_and_association(self):
        self.assertEqual(self.show("1 + 2 * 3"), "1 + 2 * 3")
        self.assertEqual(self.show("(1 + 2) * 3"), "(1 + 2) * 3")
        self.assertEqual(self.show("1 - 2 - 3"), "1 - 2 - 3")
        self.assertEqual(self.show("-x"), "0 - x")
        self.assertEqual(self.show("a + 1 < b * 2"), "a + 1 < b * 2")

    def test_errors_without_leaks(self):
        self.assertEqual(self.show("1 +"), "1:4")
        self.assertEqual(self.show("(1 + 2"), "1:7")
        self.assertEqual(self.show("1 < 2 < 3"), "1:7")
        self.assertEqual(self.show("a * (b + * c)"), "1:10")


@unittest.skipUnless(SOLUTION.exists(), "falta la solución")
class WhatTheNotesSayOfTheInterpreter(unittest.TestCase):
    PROGRAM = "lab/programs/{}.mini"

    def test_runaway_recursion_overflows_the_c_stack(self):
        # En x86-64 emulado, ASan a veces muere sin informe: también vale una señal.
        out, code = run(BUILD / "solution" / "mini", self.PROGRAM.format("runaway"))
        self.assertNotEqual(code, 0)
        if "AddressSanitizer" in out:
            self.assertIn("stack-overflow", out)
            self.assertIn("eval_call", out)
            self.assertNotIn("forever", out)

    def test_my_if_never_reaches_its_base_case(self):
        out, code = run(BUILD / "solution" / "mini", self.PROGRAM.format("my_if"))
        self.assertNotEqual(code, 0)
        self.assertNotIn("6\n", out.split("AddressSanitizer")[0])

    def test_without_sanitizers_it_is_a_signal(self):
        with tempfile.TemporaryDirectory() as folder:
            solution_copy(folder)
            binary = compile_all(folder, [*BASE, "-g", "-O0"], "plain")
            _, code = run(binary, self.PROGRAM.format("runaway"))
        self.assertIn(code, (-11, 139))

    def test_a_forgotten_env_free_leaks_three_blocks_per_frame(self):
        def forget(text):
            old = "    env_free(callee);\n    return ok;"
            self.assertIn(old, text)
            return text.replace(old, "    return ok;")

        with tempfile.TemporaryDirectory() as folder:
            solution_copy(folder, forget)
            binary = compile_all(folder, FLAGS, "leaky")
            out, code = run(binary, self.PROGRAM.format("fact"))
        self.assertNotEqual(code, 0)
        self.assertIn("in 15 allocation(s)", out)

    def test_the_frame_of_the_caller_changes_the_result(self):
        def wrong_parent(text):
            old = "env_new(in->global);\n    if (callee"
            self.assertIn(old, text)
            return text.replace(old, "env_new((Env *) frame);\n    if (callee")

        with tempfile.TemporaryDirectory() as folder:
            solution_copy(folder, wrong_parent)
            binary = compile_all(folder, FLAGS, "dynamic")
            out, _ = run(binary, SOLUTION / "mine" / "frames.mini")
            self.assertEqual(out.strip(), "100")
            out, _ = run(binary, self.PROGRAM.format("fact"))
            self.assertEqual(out.strip(), "120")
            out, _ = run(binary, self.PROGRAM.format("fib"))
            self.assertEqual(out.split(), ["1", "55", "6765"])
        out, _ = run(BUILD / "solution" / "mini", SOLUTION / "mine" / "frames.mini")
        self.assertEqual(out.strip(), "20")

    def test_argument_in_the_new_frame_gives_the_wrong_result(self):
        def args_in_new_frame(text):
            old = "        if (!env_set(callee, function->as.def.params[i], values[i])) {"
            self.assertIn(old, text)
            return text.replace(old, "        if (!eval(in, e->as.call.args[i], callee, &values[i])) { return 0; }\n" + old)

        program = "def sub(a, b) = a - b\nlet a = 10\nlet b = 3\nprint sub(b, a)\n"
        with tempfile.TemporaryDirectory() as folder:
            solution_copy(folder, args_in_new_frame)
            binary = compile_all(folder, FLAGS, "wrong_args")
            path = pathlib.Path(folder) / "p.mini"
            path.write_text(program, encoding="utf-8")
            self.assertEqual(run(binary, path)[0].strip(), "0")
        with tempfile.TemporaryDirectory() as folder:
            path = pathlib.Path(folder) / "p.mini"
            path.write_text(program, encoding="utf-8")
            self.assertEqual(run(BUILD / "solution" / "mini", path)[0].strip(), "-7")

    def test_two_stacks_in_gdb(self):
        if shutil.which("gdb") is None:
            self.skipTest("no hay gdb")
        with tempfile.TemporaryDirectory() as folder:
            solution_copy(folder)
            binary = compile_all(folder, [*BASE, "-g", "-O0"], "with_gdb")
            out, _ = run("gdb", "-q", "-batch", "-ex", "set print frame-arguments none",
                         "-ex", "break eval if e->kind == EXPR_NUMBER && e->as.number == 1", "-ex", "ignore 1 2",
                         "-ex", f"run {self.PROGRAM.format('fact3')}", "-ex", "backtrace", binary)
        if " main " not in out:
            self.skipTest("gdb no puede correr programas aquí")
        frames = [line for line in out.splitlines() if line.startswith("#")]
        self.assertEqual(sum(" eval_call " in line for line in frames), 3)
        self.assertEqual(len(frames), 20)


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.

Son de varios niveles: programas de Mini completos, errores esperados, y un main en C que mira
los árboles por la interfaz. Todo corre con ASan y UBSan: una fuga o un aviso hacen fallar la prueba.
"""
import os
import pathlib
import re
import subprocess
import tempfile
import unittest

HERE = pathlib.Path(__file__).parent
LAB_DIR = os.environ.get("LAB_DIR", "solution")
SOURCE = HERE / LAB_DIR
LAB = HERE / "lab"
FLAGS = ["-Wall", "-Wextra", "-std=c17", "-g", "-fsanitize=address,undefined"]
MODULES = ["env.c", "expr.c", "lexer.c", "stmt.c", "parser.c", "interp.c"]
REPORTS = ("AddressSanitizer", "LeakSanitizer", "runtime error")
FOLDER = tempfile.TemporaryDirectory()
BUILT = {}


def source(name):
    """El archivo de la carpeta que se prueba. Si no está ahí, es uno de los ya hechos de lab/."""
    path = SOURCE / name
    return path if path.exists() else LAB / name


def compile_program(name, sources):
    """Compila una sola vez cada programa, y devuelve su ruta."""
    if name not in BUILT:
        binary = pathlib.Path(FOLDER.name) / name
        done = subprocess.run(
            ["gcc", *FLAGS, f"-I{LAB}", *map(str, sources), "-o", str(binary)],
            text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
        )
        BUILT[name] = (binary, done.returncode, done.stdout)
    binary, code, messages = BUILT[name]
    if code != 0:
        raise AssertionError(f"No compila:\n{messages}")
    if messages:
        raise AssertionError(f"Compila con advertencias, y no debe:\n{messages}")
    return binary


def mini():
    return compile_program("mini", [source(m) for m in MODULES] + [LAB / "mini.c"])


def run_binary(binary, *args):
    try:
        done = subprocess.run(
            [str(binary), *map(str, args)], text=True, encoding="utf-8", errors="replace",
            stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=60,
        )
    except subprocess.TimeoutExpired:
        raise AssertionError("El programa no terminó en 60 segundos.") from None
    return done.stdout, done.stderr, done.returncode


def run_mini(program):
    """Corre el texto de un programa de Mini. Devuelve (salida, errores, código)."""
    path = pathlib.Path(FOLDER.name) / "program.mini"
    path.write_text(program, encoding="utf-8")
    return run_binary(mini(), path)


class MiniCase(unittest.TestCase):
    longMessage = False

    @classmethod
    def setUpClass(cls):
        if not SOURCE.exists():
            raise unittest.SkipTest(f"no está {LAB_DIR}")

    def no_reports(self, program, out, err):
        for word in REPORTS:
            if word in out or word in err:
                lines = err.splitlines()[:14]
                self.fail(f"Hay un informe de {word} con este programa:\n{program}\n" + "\n".join(lines))

    def prints(self, program, wanted):
        out, err, code = run_mini(program)
        self.no_reports(program, out, err)
        if code != 0:
            self.fail(f"Terminó con error, y no debía:\n{program}\n{err}")
        got = out.split()
        if got != wanted.split():
            self.fail(f"Este programa:\n{program}\n  debe escribir: {wanted}\n  escribió:       {' '.join(got)}")

    def run_error(self, program, words, printed=""):
        out, err, code = run_mini(program)
        self.no_reports(program, out, err)
        if code != 1:
            self.fail(f"Debe terminar con un error de ejecución:\n{program}\n  escribió: {out}")
        if "error de ejecución" not in err:
            self.fail(f"Debe ser un error de ejecución:\n{program}\n  se obtuvo: {err}")
        for word in words:
            if word not in err:
                self.fail(f"El mensaje debe decir '{word}':\n{program}\n  se obtuvo: {err}")
        if out.split() != printed.split():
            self.fail(f"Antes del error debe escribir: {printed}\n{program}\n  escribió: {out}")

    def syntax_error(self, program, line=None):
        out, err, code = run_mini(program)
        self.no_reports(program, out, err)
        if code != 1 or "error de sintaxis" not in err:
            self.fail(f"Debe dar un error de sintaxis:\n{program}\n  se obtuvo: {out}{err}")
        if "falta " in err:
            self.fail(f"Aún falta escribir una parte: {err}")
        if out:
            self.fail(f"Con un error de sintaxis no se corre nada:\n{program}\n  escribió: {out}")
        if line is not None and f":{line}:" not in err:
            self.fail(f"El error debe estar en la línea {line}:\n{program}\n  se obtuvo: {err}")


class LetAndPrint(MiniCase):
    def test_print(self):
        self.prints("print 1 + 2 * 3", "7")
        self.prints("print (1 + 2) * 3", "9")
        self.prints("print -2 - -3", "1")
        self.prints("print 0.5", "0.5")
        self.prints("print 1 / 3", "0.333333")

    def test_several_prints_in_order(self):
        self.prints("print 1\nprint 2\nprint 3\n", "1 2 3")

    def test_let(self):
        self.prints("let x = 4\nprint x * x", "16")

    def test_let_changes_the_value(self):
        self.prints("let x = 1\nlet x = x + 1\nprint x", "2")

    def test_no_separator_is_needed(self):
        self.prints("let a = 1 let b = a + 1 print a + b", "3")

    def test_comments_and_blank_lines(self):
        self.prints("# hola\n\nlet a = 2 # dos\n\n# fin\nprint a # listo\n", "2")

    def test_comparisons_print_one_or_zero(self):
        self.prints("print 1 < 2\nprint 2 < 1\nprint 2 <= 2\nprint 3 > 4\nprint 4 >= 4\nprint 1 == 1\nprint 1 != 1",
                    "1 0 1 0 1 1 0")

    def test_empty_program(self):
        self.prints("", "")
        self.prints("# solo un comentario\n", "")


class IfExpression(MiniCase):
    def test_both_branches(self):
        self.prints("print if 1 < 2 then 10 else 20", "10")
        self.prints("print if 2 < 1 then 10 else 20", "20")

    def test_any_value_except_zero_is_true(self):
        self.prints("print if 0.5 then 1 else 2\nprint if -3 then 1 else 2\nprint if 0 then 1 else 2", "1 1 2")

    def test_only_the_chosen_branch_runs(self):
        self.prints("print if 1 then 2 else 1 / 0", "2")
        self.prints("print if 0 then 1 / 0 else 3", "3")
        self.prints("print if 1 then 2 else missing", "2")

    def test_nested(self):
        self.prints("let x = 5\nprint if x < 3 then 1 else if x < 6 then 2 else 3", "2")
        self.prints("print if if 1 then 0 else 1 then 10 else 20", "20")

    def test_inside_parentheses(self):
        self.prints("print 1 + (if 1 then 2 else 3) * 4", "9")

    def test_else_reaches_as_far_as_it_can(self):
        self.prints("print if 1 then 2 else 3 + 4\nprint if 0 then 2 else 3 + 4", "2 7")

    def test_condition_errors(self):
        self.run_error("print if missing then 1 else 2", ["missing"])


class Calls(MiniCase):
    def test_required_example(self):
        self.prints("def fact(n) = if n < 2 then 1 else n * fact(n - 1)\nlet x = 5\nprint fact(x)     # 120\n", "120")

    def test_no_parameters(self):
        self.prints("def seven() = 7\nprint seven() + 1", "8")

    def test_several_parameters(self):
        self.prints("def sub(a, b) = a - b\nprint sub(10, 3)", "7")

    def test_more_arguments_than_any_first_capacity(self):
        names = [f"p{i}" for i in range(20)]
        args = ", ".join(str(i + 1) for i in range(20))
        self.prints(f"def total({', '.join(names)}) = {' + '.join(names)}\nprint total({args})", "210")

    def test_calls_inside_arguments(self):
        self.prints("def inc(n) = n + 1\ndef add(a, b) = a + b\nprint add(inc(1), inc(inc(1)))", "5")

    def test_if_as_argument(self):
        self.prints("def id(n) = n\nprint id(if 1 then 2 else 3)", "2")

    def test_arguments_use_the_frame_of_the_caller(self):
        self.prints("def sub(a, b) = a - b\nlet a = 10\nlet b = 3\nprint sub(b, a)", "-7")
        self.prints("def sub(a, b) = a - b\ndef swap(a, b) = sub(b, a)\nprint swap(1, 5)", "4")

    def test_parameter_covers_a_global(self):
        self.prints("let x = 10\ndef double(x) = x * 2\nprint double(3)\nprint x", "6 10")

    def test_body_reads_globals_when_it_runs(self):
        self.prints("let r = 2\ndef scale(n) = n * r\nprint scale(5)\nlet r = 3\nprint scale(5)", "10 15")

    def test_body_does_not_see_the_locals_of_its_caller(self):
        self.run_error("def show() = n\ndef outer(n) = show()\nprint outer(7)", ["variable desconocida", "n"])

    def test_after_a_call_the_parameter_is_gone(self):
        self.run_error("def id(n) = n\nprint id(1)\nprint n", ["variable desconocida", "n"], "1")

    def test_functions_and_variables_are_separate(self):
        self.prints("let f = 1\ndef f(x) = x + 1\nprint f(f)", "2")

    def test_def_takes_effect_when_it_runs(self):
        self.run_error("print 1\nprint twice(4)\ndef twice(n) = n * 2", ["función desconocida", "twice"], "1")

    def test_body_may_name_a_function_defined_later(self):
        self.prints("def even(n) = if n == 0 then 1 else odd(n - 1)\n"
                    "def odd(n) = if n == 0 then 0 else even(n - 1)\n"
                    "print even(10)\nprint even(7)", "1 0")

    def test_a_new_def_replaces_the_old_one(self):
        self.prints("def f() = 1\nprint f()\ndef f() = 2\nprint f()", "1 2")


class Programs(MiniCase):
    """Los programas de lab/programs, enteros."""

    def run_file(self, name, wanted):
        self.prints((LAB / "programs" / name).read_text(encoding="utf-8"), wanted)

    def test_factorial(self):
        self.run_file("fact.mini", "120")

    def test_large_factorial_goes_through_percent_g(self):
        self.run_file("big.mini", "720 3.6288e+06 6.22702e+09")

    def test_fibonacci(self):
        self.run_file("fib.mini", "1 55 6765")

    def test_mutual_recursion(self):
        self.run_file("even_odd.mini", "1 0")

    def test_a_thousand_mini_frames(self):
        self.run_file("deep.mini", "500500")

    def test_many_calls_leave_no_leaks(self):
        self.prints("def fib(n) = if n < 2 then n else fib(n - 1) + fib(n - 2)\nprint fib(22)", "17711")


class RunErrors(MiniCase):
    def test_unknown_variable(self):
        self.run_error("print 1\nprint x\nprint 2", ["variable desconocida", "x"], "1")

    def test_unknown_function(self):
        self.run_error("print f(1)", ["función desconocida", "f"])

    def test_too_few_arguments(self):
        self.run_error("def add(a, b) = a + b\nprint add(1)", ["argumentos", "add"])

    def test_too_many_arguments(self):
        self.run_error("def add(a, b) = a + b\nprint add(1, 2, 3)", ["argumentos", "add"])

    def test_arguments_for_no_parameters(self):
        self.run_error("def f() = 1\nprint f(2)", ["argumentos", "f"])

    def test_division_by_zero(self):
        self.run_error("print 1 / 0", ["división entre cero"])
        self.run_error("let x = 0\nlet y = 1 / x", ["división entre cero"])

    def test_error_in_an_argument_frees_the_values(self):
        self.run_error("def f(a, b, c) = a\nprint f(1, 2 / 0, 3)", ["división entre cero"])
        self.run_error("def f(a, b, c) = a\nprint f(1, 2, missing)", ["variable desconocida"])

    def test_error_deep_inside_frees_every_frame(self):
        self.run_error("def down(n, m) = if n == 0 then 1 / 0 else 1 + down(n - 1, m)\nprint down(50, 1)",
                       ["división entre cero"])
        self.run_error("def down(n, m) = if n == 0 then nope(1, 2) else n + down(n - 1, m)\nprint down(50, 1)",
                       ["función desconocida", "nope"])

    def test_error_in_a_nested_argument(self):
        self.run_error("def id(n) = n\ndef two(a, b) = a + b\nprint two(id(1), id(two(id(2), id(missing))))",
                       ["variable desconocida", "missing"])


class SyntaxErrors(MiniCase):
    def test_statements(self):
        for program in ("let", "let x", "let x =", "let 3 = 1", "let if = 1", "print", "x = 1", "1 + 2"):
            with self.subTest(program=program):
                self.syntax_error(program)

    def test_if(self):
        for program in ("print if", "print if 1", "print if 1 then", "print if 1 then 2", "print if 1 then 2 else",
                        "print 1 + if 1 then 2 else 3", "print if 1 else 2 then 3"):
            with self.subTest(program=program):
                self.syntax_error(program)

    def test_if_frees_what_it_read(self):
        for program in ("print if a + 1 then", "print if a + 1 then b + 2", "print if a + 1 then b + 2 else *",
                        "print if a + 1 + then 2 else 3", "print if a then b + * else 3"):
            with self.subTest(program=program):
                self.syntax_error(program)

    def test_call(self):
        for program in ("print f(", "print f(1", "print f(1,", "print f(1,)", "print f(,1)", "print f(1 2)",
                        "print f(a + 1, b * 2, c - 3", "print f(a + 1, b * 2, g(c, d), e + ) + 1",
                        "print f(1, 2, 3, 4, 5, 6, 7, 8, 9, *)"):
            with self.subTest(program=program):
                self.syntax_error(program)

    def test_def(self):
        for program in ("def", "def 1() = 2", "def f", "def f(", "def f(a", "def f(a,", "def f(a,) = 1", "def f(1) = 2",
                        "def f(a, b)", "def f(a, b) 1", "def f(a, b) =", "def f(a, b) = a +", "def if() = 1",
                        "def f(a, b, c, d, e, g, h) = *"):
            with self.subTest(program=program):
                self.syntax_error(program)

    def test_comparisons_do_not_chain(self):
        self.syntax_error("print 1 < 2 < 3")

    def test_an_error_after_good_statements_frees_them(self):
        self.syntax_error("def f(a) = a + 1\nlet x = f(2)\nprint x\nprint (", line=4)

    def test_position_of_the_error(self):
        self.syntax_error("let x = 1\n\nprint x +\n", line=4)
        self.syntax_error("let x = 1\nlet = 2\n", line=2)


API_MAIN = r"""
#include <stdio.h>
#include <string.h>
#include "interp.h"
#include "parser.h"

static int shows(const char *source) {
    ParseError error;
    Stmt *program = parse_program(source, &error);
    if (program == NULL) {
        printf("%d:%s\n", error.line, error.message[0] != '\0' ? "error" : "vacio");
        return 0;
    }
    for (const Stmt *s = program; s != NULL; s = s->next) {
        switch (s->kind) {
        case STMT_LET:
            printf("let %s ", s->as.let.name);
            expr_print(s->as.let.value, stdout);
            break;
        case STMT_PRINT:
            printf("print ");
            expr_print(s->as.print.value, stdout);
            break;
        case STMT_DEF:
            printf("def %s %d", s->as.def.name, s->as.def.param_count);
            for (int i = 0; i < s->as.def.param_count; i++) {
                printf(" %s", s->as.def.params[i]);
            }
            printf(" ");
            expr_print(s->as.def.body, stdout);
            break;
        }
        printf("\n");
    }
    stmt_free(program);
    return 1;
}

static void runs_on(const char *source) {
    ParseError syntax;
    RunError error;
    Stmt *program = parse_program(source, &syntax);
    FILE *out = tmpfile();
    if (out == NULL) {
        printf("sin tmpfile\n");
        stmt_free(program);
        return;
    }
    int ok = run_program(program, out, &error);
    rewind(out);
    int c;
    printf("%d [", ok);
    while ((c = fgetc(out)) != EOF) {
        putchar(c == '\n' ? ' ' : c);
    }
    printf("] %s\n", ok ? (error.message[0] == '\0' ? "sin-mensaje" : "con-mensaje") : "error");
    fclose(out);
    stmt_free(program);
}

int main(void) {
    shows("");
    shows("let\n");
    shows("let a = 1\nprint a + 2\ndef f(x, y) = g(x, 1) * (if y then 2 else 3)\ndef h() = 0\n");
    runs_on("print 1 print 2");
    runs_on("");
    runs_on("print 1 print 1 / 0 print 2");
    // Un programa se puede correr dos veces: run_program no se lo apropia.
    ParseError syntax;
    RunError error;
    Stmt *program = parse_program("def f() = 1 print f()", &syntax);
    FILE *out = tmpfile();
    if (out != NULL) {
        int first = run_program(program, out, &error);
        int second = run_program(program, out, &error);
        printf("%d %d\n", first, second);
        fclose(out);
    }
    stmt_free(program);
    return 0;
}
"""


class Interface(MiniCase):
    """Lo mismo, pero por la interfaz de parser.h e interp.h."""

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        path = pathlib.Path(FOLDER.name) / "api_main.c"
        path.write_text(API_MAIN, encoding="utf-8")
        cls.main_path = path

    def test_trees_and_results(self):
        binary = compile_program("api", [self.main_path] + [source(m) for m in MODULES])
        out, err, code = run_binary(binary)
        self.no_reports("(main de prueba)", out, err)
        self.assertEqual(code, 0, err)
        wanted = [
            "0:vacio",
            "2:error",
            "let a 1",
            "print a + 2",
            "def f 2 x y g(x, 1) * (if y then 2 else 3)",
            "def h 0 0",
            "1 [1 2 ] sin-mensaje",
            "1 [] sin-mensaje",
            "0 [1 ] error",
            "1 1",
        ]
        got = out.splitlines()
        if got != wanted:
            self.fail("La interfaz no da lo esperado.\n  se esperaba:\n    " + "\n    ".join(wanted)
                      + "\n  se obtuvo:\n    " + "\n    ".join(got))

    def test_no_writable_globals(self):
        for name in ("parser.c", "interp.c"):
            with tempfile.TemporaryDirectory() as folder:
                obj = pathlib.Path(folder) / "file.o"
                subprocess.run(["gcc", "-std=c17", "-fno-pic", f"-I{LAB}", "-c", str(source(name)), "-o", str(obj)], check=True)
                done = subprocess.run(["nm", str(obj)], text=True, stdout=subprocess.PIPE, check=True)
            found = [line.split()[-1] for line in done.stdout.splitlines() if re.search(r" [bBdD] ", line)]
            with self.subTest(file=name):
                self.assertEqual(found, [], f"{name} no debe tener variables globales: {found}")


class OwnPrograms(MiniCase):
    """Sus programas de mine/: cada x.mini con su x.expected, que dice qué debe escribir."""

    def programs(self):
        folder = SOURCE / "mine"
        return sorted(path for path in folder.glob("*.mini") if path.stem != "example")

    def test_there_are_at_least_two(self):
        self.assertGreaterEqual(len(self.programs()), 2,
                                "Falta la parte 5: agreguen a lab/mine al menos dos programas con su .expected.")

    def test_every_program_gives_what_it_expects(self):
        for path in sorted((SOURCE / "mine").glob("*.mini")):
            with self.subTest(program=path.name):
                expected = path.with_suffix(".expected")
                self.assertTrue(expected.exists(), f"Falta {expected.name}.")
                self.prints(path.read_text(encoding="utf-8"), expected.read_text(encoding="utf-8"))


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"
# Antes de un bloque cuya salida cambia con el sistema. No se compara.
SYSTEM_DEPENDENT = "<!-- depende del sistema -->"
# Antes de un extracto de un informe de ASan, UBSan o valgrind: sus líneas deben aparecer
# en la salida real y en ese orden, sin contar direcciones, números de proceso ni rutas.
REPORT = "<!-- extracto de un informe -->"
# Antes de lo que debe escribir el laboratorio: se corre con la solución, en build/solution.
LAB_OUTPUT = "<!-- salida del laboratorio -->"


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


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


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


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


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


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


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


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


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


class NotesAreTrue(unittest.TestCase):
    @unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
    def test_terminal_blocks_in_notes(self):
        checked = 0
        for lines, mark in note_blocks("text"):
            if not lines or not lines[0].startswith("$ ") or mark == LAB_OUTPUT:
                continue
            for command, expected in split_commands(lines):
                actual = clean_lines(run_shell(command))
                checked += 1
                if mark == REPORT:
                    assert_in_order(self, [stable_report_line(line) for line in expected],
                                    [stable_report_line(line) for line in actual], command)
                elif mark != SYSTEM_DEPENDENT:
                    with self.subTest(command=command):
                        self.assertEqual(actual, expected)
        self.assertGreater(checked, 0)

    @unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
    def test_code_blocks_in_notes_exist(self):
        known = known_code_lines()
        for lines, _ in note_blocks("c"):
            for line in lines:
                if line.strip() and line.strip() != "...":
                    with self.subTest(line=line):
                        self.assertIn(line.strip(), known)

    @unittest.skipUnless(NOTES.exists() and (HERE / "solution").exists(), "falta la solución")
    def test_lab_output_in_notes(self):
        checked = 0
        for lines, mark in note_blocks("text"):
            if mark == LAB_OUTPUT:
                command = lines[0][2:].replace("build/lab/", "build/solution/")
                actual = clean_lines(run_shell(command))
                self.assertEqual(actual, [line.rstrip() for line in lines[1:]])
                checked += 1
        self.assertGreater(checked, 10)

    @unittest.skipUnless(NOTES.exists() and (HERE / "solution").exists(), "falta la solución")
    def test_programs_quoted_in_the_notes_exist(self):
        # Cada programa de Mini citado en los apuntes está, tal cual, en una carpeta de programas.
        folders = [HERE / "lab" / "programs", HERE / "solution" / "mine"]
        known = {path.read_text(encoding="utf-8").strip() for folder in folders for path in folder.glob("*.mini")}
        marks = ("def ", "let ", "print ", "# ")
        for lines, _ in note_blocks("text"):
            text = "\n".join(lines).strip()
            if text and lines[0].startswith(marks) and len(lines) >= 3:
                with self.subTest(program=lines[0]):
                    self.assertIn(text, known)


class SlidesAreTrue(unittest.TestCase):
    @unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
    def test_terminal_blocks_in_slides(self):
        # Los informes van en otro entorno, informe, que no se compara. Lo que usa el intérprete
        # terminado se corre con la solución; sin ella, no se comprueba.
        has_solution = (HERE / "solution").exists()
        for block in slide_blocks("terminal|salida|salidalarga"):
            for command, expected in split_commands(block):
                if "build/lab/" in command:
                    if not has_solution:
                        continue
                    command = command.replace("build/lab/", "build/solution/")
                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/env.cDescargar lab/env.c
#include "env.h"

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

struct binding {
    char *name;
    double value;
    struct binding *next;
};

struct env {
    Env *parent;
    struct binding *first;
};

Env *env_new(Env *parent) {
    Env *env = malloc(sizeof *env);
    if (env == NULL) {
        return NULL;
    }
    env->parent = parent;
    env->first = NULL;
    return env;
}

void env_free(Env *env) {
    if (env == NULL) {
        return;
    }
    struct binding *current = env->first;
    while (current != NULL) {
        struct binding *next = current->next;
        free(current->name);
        free(current);
        current = next;
    }
    free(env);
}

int env_set(Env *env, const char *name, double value) {
    for (struct binding *b = env->first; b != NULL; b = b->next) {
        if (strcmp(b->name, name) == 0) {
            b->value = value;
            return 1;
        }
    }
    struct binding *b = malloc(sizeof *b);
    if (b == NULL) {
        return 0;
    }
    b->name = malloc(strlen(name) + 1);
    if (b->name == NULL) {
        free(b);
        return 0;
    }
    strcpy(b->name, name);
    b->value = value;
    b->next = env->first;
    env->first = b;
    return 1;
}

int env_get(const Env *env, const char *name, double *value) {
    for (const Env *e = env; e != NULL; e = e->parent) {
        for (const struct binding *b = e->first; b != NULL; b = b->next) {
            if (strcmp(b->name, name) == 0) {
                *value = b->value;
                return 1;
            }
        }
    }
    return 0;
}
lab/env.hDescargar lab/env.h
// Tabla de variables de un marco. Se usa solo por esta interfaz.
//
// Owner: quien llama a env_new libera con env_free. env_free no libera al padre.
#ifndef ENV_H
#define ENV_H

typedef struct env Env;

// Crea un marco vacío. parent puede ser NULL. Devuelve NULL si no hay memoria.
Env *env_new(Env *parent);

// Libera el marco y sus nombres. Acepta NULL.
void env_free(Env *env);

// Define name en este marco, o cambia su valor si ya está en este marco.
// Copia el nombre. Devuelve 1 si pudo y 0 si no hay memoria.
int env_set(Env *env, const char *name, double value);

// Busca name en este marco y luego en sus padres. Si lo encuentra, guarda el
// valor en *value y devuelve 1. Si no, devuelve 0 y no toca *value.
int env_get(const Env *env, const char *name, double *value);

#endif
lab/expr.cDescargar lab/expr.c
#include "expr.h"

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

Expr *expr_number(double value) {
    Expr *e = malloc(sizeof *e);
    if (e == NULL) {
        return NULL;
    }
    e->kind = EXPR_NUMBER;
    e->as.number = value;
    return e;
}

Expr *expr_variable(const char *name) {
    Expr *e = malloc(sizeof *e);
    if (e == NULL) {
        return NULL;
    }
    e->as.name = malloc(strlen(name) + 1);
    if (e->as.name == NULL) {
        free(e);
        return NULL;
    }
    strcpy(e->as.name, name);
    e->kind = EXPR_VARIABLE;
    return e;
}

Expr *expr_binary(BinaryOp op, Expr *left, Expr *right) {
    Expr *e = NULL;
    if (left != NULL && right != NULL) {
        e = malloc(sizeof *e);
    }
    if (e == NULL) {
        expr_free(left);
        expr_free(right);
        return NULL;
    }
    e->kind = EXPR_BINARY;
    e->as.binary.op = op;
    e->as.binary.left = left;
    e->as.binary.right = right;
    return e;
}

Expr *expr_if(Expr *condition, Expr *then_branch, Expr *else_branch) {
    Expr *e = NULL;
    if (condition != NULL && then_branch != NULL && else_branch != NULL) {
        e = malloc(sizeof *e);
    }
    if (e == NULL) {
        expr_free(condition);
        expr_free(then_branch);
        expr_free(else_branch);
        return NULL;
    }
    e->kind = EXPR_IF;
    e->as.conditional.condition = condition;
    e->as.conditional.then_branch = then_branch;
    e->as.conditional.else_branch = else_branch;
    return e;
}

static void free_args(Expr **args, int arg_count) {
    for (int i = 0; i < arg_count; i++) {
        expr_free(args[i]);
    }
    free(args);
}

Expr *expr_call(const char *name, Expr **args, int arg_count) {
    int complete = 1;
    for (int i = 0; i < arg_count; i++) {
        if (args[i] == NULL) {
            complete = 0;
        }
    }
    Expr *e = complete ? malloc(sizeof *e) : NULL;
    char *copy = complete ? malloc(strlen(name) + 1) : NULL;
    if (e == NULL || copy == NULL) {
        free(e);
        free(copy);
        free_args(args, arg_count);
        return NULL;
    }
    strcpy(copy, name);
    e->kind = EXPR_CALL;
    e->as.call.name = copy;
    e->as.call.args = args;
    e->as.call.arg_count = arg_count;
    return e;
}

void expr_free(Expr *e) {
    if (e == NULL) {
        return;
    }
    switch (e->kind) {
        case EXPR_NUMBER:
            break;
        case EXPR_VARIABLE:
            free(e->as.name);
            break;
        case EXPR_BINARY:
            expr_free(e->as.binary.left);
            expr_free(e->as.binary.right);
            break;
        case EXPR_IF:
            expr_free(e->as.conditional.condition);
            expr_free(e->as.conditional.then_branch);
            expr_free(e->as.conditional.else_branch);
            break;
        case EXPR_CALL:
            free(e->as.call.name);
            free_args(e->as.call.args, e->as.call.arg_count);
            break;
    }
    free(e);
}

static const char *op_symbol(BinaryOp op) {
    switch (op) {
        case OP_ADD: return "+";
        case OP_SUB: return "-";
        case OP_MUL: return "*";
        case OP_DIV: return "/";
        case OP_LESS: return "<";
        case OP_LESS_EQUAL: return "<=";
        case OP_GREATER: return ">";
        case OP_GREATER_EQUAL: return ">=";
        case OP_EQUAL: return "==";
        case OP_NOT_EQUAL: return "!=";
    }
    return "?";
}

// A mayor número, más fuerte: el nivel de la gramática.
static int precedence(BinaryOp op) {
    switch (op) {
        case OP_MUL:
        case OP_DIV:
            return 3;
        case OP_ADD:
        case OP_SUB:
            return 2;
        case OP_LESS:
        case OP_LESS_EQUAL:
        case OP_GREATER:
        case OP_GREATER_EQUAL:
        case OP_EQUAL:
        case OP_NOT_EQUAL:
            return 1;
    }
    return 0;
}

// Escribe un operando, entre paréntesis si su nivel es menor que minimum. Un if siempre los
// lleva: en la gramática, solo entra en una operación con paréntesis.
static void print_child(const Expr *child, int minimum, FILE *out) {
    int needs_parens = child->kind == EXPR_IF
        || (child->kind == EXPR_BINARY && precedence(child->as.binary.op) < minimum);
    if (needs_parens) {
        fputc('(', out);
    }
    expr_print(child, out);
    if (needs_parens) {
        fputc(')', out);
    }
}

void expr_print(const Expr *e, FILE *out) {
    switch (e->kind) {
        case EXPR_NUMBER:
            fprintf(out, "%g", e->as.number);
            break;
        case EXPR_VARIABLE:
            fputs(e->as.name, out);
            break;
        case EXPR_BINARY: {
            int level = precedence(e->as.binary.op);
            // Asocian por la izquierda: a la derecha, el mismo nivel lleva paréntesis.
            // Las comparaciones no se encadenan: ahí los lleva a los dos lados.
            print_child(e->as.binary.left, level == 1 ? 2 : level, out);
            fprintf(out, " %s ", op_symbol(e->as.binary.op));
            print_child(e->as.binary.right, level + 1, out);
            break;
        }
        case EXPR_IF:
            fputs("if ", out);
            expr_print(e->as.conditional.condition, out);
            fputs(" then ", out);
            expr_print(e->as.conditional.then_branch, out);
            fputs(" else ", out);
            expr_print(e->as.conditional.else_branch, out);
            break;
        case EXPR_CALL:
            fprintf(out, "%s(", e->as.call.name);
            for (int i = 0; i < e->as.call.arg_count; i++) {
                if (i > 0) {
                    fputs(", ", out);
                }
                expr_print(e->as.call.args[i], out);
            }
            fputc(')', out);
            break;
    }
}

int expr_apply(BinaryOp op, double a, double b, double *result) {
    switch (op) {
        case OP_ADD: *result = a + b; return 1;
        case OP_SUB: *result = a - b; return 1;
        case OP_MUL: *result = a * b; return 1;
        case OP_DIV:
            if (b == 0.0) {
                return 0;
            }
            *result = a / b;
            return 1;
        case OP_LESS: *result = a < b; return 1;
        case OP_LESS_EQUAL: *result = a <= b; return 1;
        case OP_GREATER: *result = a > b; return 1;
        case OP_GREATER_EQUAL: *result = a >= b; return 1;
        case OP_EQUAL: *result = a == b; return 1;
        case OP_NOT_EQUAL: *result = a != b; return 1;
    }
    return 0;
}

int expr_eval(const Expr *e, const Env *env, double *result) {
    switch (e->kind) {
        case EXPR_NUMBER:
            *result = e->as.number;
            return 1;
        case EXPR_VARIABLE:
            return env_get(env, e->as.name, result);
        case EXPR_BINARY: {
            double left;
            double right;
            if (!expr_eval(e->as.binary.left, env, &left) || !expr_eval(e->as.binary.right, env, &right)) {
                return 0;
            }
            return expr_apply(e->as.binary.op, left, right, result);
        }
        case EXPR_IF: {
            double condition;
            if (!expr_eval(e->as.conditional.condition, env, &condition)) {
                return 0;
            }
            return expr_eval(condition != 0 ? e->as.conditional.then_branch : e->as.conditional.else_branch, env, result);
        }
        case EXPR_CALL:
            return 0;
    }
    return 0;
}

Expr *expr_copy(const Expr *e) {
    if (e == NULL) {
        return NULL;
    }
    switch (e->kind) {
        case EXPR_NUMBER:
            return expr_number(e->as.number);
        case EXPR_VARIABLE:
            return expr_variable(e->as.name);
        case EXPR_BINARY:
            return expr_binary(e->as.binary.op, expr_copy(e->as.binary.left), expr_copy(e->as.binary.right));
        case EXPR_IF:
            return expr_if(expr_copy(e->as.conditional.condition), expr_copy(e->as.conditional.then_branch),
                           expr_copy(e->as.conditional.else_branch));
        case EXPR_CALL: {
            int count = e->as.call.arg_count;
            Expr **args = NULL;
            if (count > 0) {
                args = malloc((size_t) count * sizeof *args);
                if (args == NULL) {
                    return NULL;
                }
            }
            for (int i = 0; i < count; i++) {
                args[i] = expr_copy(e->as.call.args[i]);
            }
            return expr_call(e->as.call.name, args, count);
        }
    }
    return NULL;
}

int expr_equal(const Expr *a, const Expr *b) {
    if (a->kind != b->kind) {
        return 0;
    }
    switch (a->kind) {
        case EXPR_NUMBER:
            return a->as.number == b->as.number;
        case EXPR_VARIABLE:
            return strcmp(a->as.name, b->as.name) == 0;
        case EXPR_BINARY:
            return a->as.binary.op == b->as.binary.op && expr_equal(a->as.binary.left, b->as.binary.left)
                && expr_equal(a->as.binary.right, b->as.binary.right);
        case EXPR_IF:
            return expr_equal(a->as.conditional.condition, b->as.conditional.condition)
                && expr_equal(a->as.conditional.then_branch, b->as.conditional.then_branch)
                && expr_equal(a->as.conditional.else_branch, b->as.conditional.else_branch);
        case EXPR_CALL:
            if (strcmp(a->as.call.name, b->as.call.name) != 0 || a->as.call.arg_count != b->as.call.arg_count) {
                return 0;
            }
            for (int i = 0; i < a->as.call.arg_count; i++) {
                if (!expr_equal(a->as.call.args[i], b->as.call.args[i])) {
                    return 0;
                }
            }
            return 1;
    }
    return 0;
}

static int is_number(const Expr *e, double value) {
    return e->kind == EXPR_NUMBER && e->as.number == value;
}

// Se queda con un hijo: libera el otro y el nodo. Devuelve el hijo elegido.
static Expr *keep_child(Expr *e, Expr *kept, Expr *dropped) {
    expr_free(dropped);
    free(e);
    return kept;
}

// Convierte el nodo en un número, en su sitio, y libera sus hijos.
static Expr *become_number(Expr *e, double value) {
    expr_free(e->as.binary.left);
    expr_free(e->as.binary.right);
    e->kind = EXPR_NUMBER;
    e->as.number = value;
    return e;
}

Expr *expr_simplify(Expr *e) {
    if (e == NULL || e->kind == EXPR_NUMBER || e->kind == EXPR_VARIABLE) {
        return e;
    }
    if (e->kind == EXPR_IF) {
        e->as.conditional.condition = expr_simplify(e->as.conditional.condition);
        e->as.conditional.then_branch = expr_simplify(e->as.conditional.then_branch);
        e->as.conditional.else_branch = expr_simplify(e->as.conditional.else_branch);
        return e;
    }
    if (e->kind == EXPR_CALL) {
        for (int i = 0; i < e->as.call.arg_count; i++) {
            e->as.call.args[i] = expr_simplify(e->as.call.args[i]);
        }
        return e;
    }
    e->as.binary.left = expr_simplify(e->as.binary.left);
    e->as.binary.right = expr_simplify(e->as.binary.right);
    Expr *left = e->as.binary.left;
    Expr *right = e->as.binary.right;
    BinaryOp op = e->as.binary.op;

    if ((op == OP_ADD && is_number(left, 0.0)) || (op == OP_MUL && is_number(left, 1.0))) {
        return keep_child(e, right, left);
    }
    if ((op == OP_ADD || op == OP_SUB) && is_number(right, 0.0)) {
        return keep_child(e, left, right);
    }
    if ((op == OP_MUL || op == OP_DIV) && is_number(right, 1.0)) {
        return keep_child(e, left, right);
    }
    if (op == OP_MUL && (is_number(left, 0.0) || is_number(right, 0.0))) {
        return become_number(e, 0.0);
    }
    double value;
    if (left->kind == EXPR_NUMBER && right->kind == EXPR_NUMBER
        && expr_apply(op, left->as.number, right->as.number, &value)) {
        return become_number(e, value);
    }
    return e;
}

Expr *expr_derive(const Expr *e, const char *name) {
    switch (e->kind) {
        case EXPR_NUMBER:
            return expr_number(0.0);
        case EXPR_VARIABLE:
            return expr_number(strcmp(e->as.name, name) == 0 ? 1.0 : 0.0);
        case EXPR_BINARY:
            break;
        case EXPR_IF:
        case EXPR_CALL:
            return NULL;
    }
    const Expr *u = e->as.binary.left;
    const Expr *v = e->as.binary.right;
    switch (e->as.binary.op) {
        case OP_ADD:
        case OP_SUB:
            return expr_binary(e->as.binary.op, expr_derive(u, name), expr_derive(v, name));
        case OP_MUL:
            return expr_binary(OP_ADD,
                               expr_binary(OP_MUL, expr_derive(u, name), expr_copy(v)),
                               expr_binary(OP_MUL, expr_copy(u), expr_derive(v, name)));
        case OP_DIV:
            return expr_binary(OP_DIV,
                               expr_binary(OP_SUB,
                                           expr_binary(OP_MUL, expr_derive(u, name), expr_copy(v)),
                                           expr_binary(OP_MUL, expr_copy(u), expr_derive(v, name))),
                               expr_binary(OP_MUL, expr_copy(v), expr_copy(v)));
        case OP_LESS:
        case OP_LESS_EQUAL:
        case OP_GREATER:
        case OP_GREATER_EQUAL:
        case OP_EQUAL:
        case OP_NOT_EQUAL:
            return NULL;
    }
    return NULL;
}
lab/expr.hDescargar lab/expr.h
// El árbol de expresión de Mini: lo de la semana 13, con las variantes if y llamada.
//
// Expr no es opaco: el código hace switch sobre kind, como en la semana 7.
// Ownership: cada nodo es owner de sus hijos y de sus cadenas.
#ifndef EXPR_H
#define EXPR_H

#include <stdio.h>

#include "env.h"

typedef enum { EXPR_NUMBER, EXPR_VARIABLE, EXPR_BINARY, EXPR_IF, EXPR_CALL } ExprKind;

typedef enum {
    OP_ADD, OP_SUB, OP_MUL, OP_DIV,
    OP_LESS, OP_LESS_EQUAL, OP_GREATER, OP_GREATER_EQUAL, OP_EQUAL, OP_NOT_EQUAL
} BinaryOp;

typedef struct expr Expr;

struct expr {
    ExprKind kind;
    union {
        double number;
        char *name;                                                 // EXPR_VARIABLE: copia propia
        struct { BinaryOp op; Expr *left; Expr *right; } binary;
        struct { Expr *condition; Expr *then_branch; Expr *else_branch; } conditional;
        struct { char *name; Expr **args; int arg_count; } call;    // args es NULL si arg_count es 0
    } as;
};

// Devuelve un nodo nuevo, o NULL si no hay memoria.
Expr *expr_number(double value);

// Guarda una copia de name. Devuelve NULL si no hay memoria, sin dejar nada reservado.
Expr *expr_variable(const char *name);

// Estos tres se vuelven owners de lo que reciben, aunque fallen: en ese caso lo liberan y
// devuelven NULL. Un hijo NULL cuenta como falla.
Expr *expr_binary(BinaryOp op, Expr *left, Expr *right);
Expr *expr_if(Expr *condition, Expr *then_branch, Expr *else_branch);
Expr *expr_call(const char *name, Expr **args, int arg_count);

// Libera el árbol completo. Acepta NULL.
void expr_free(Expr *e);

// Escribe la expresión en out, con los números en %g y un espacio a cada lado del operador, sin
// salto de línea al final. Solo pone los paréntesis necesarios para conservar la forma del árbol:
//   - un hijo izquierdo los lleva si su operador tiene menor precedencia que el del padre;
//   - un hijo derecho los lleva si su operador tiene menor o igual precedencia;
//   - una comparación hija de otra los lleva siempre, y un if dentro de una operación también.
// Ejemplos: 2 + 3 * x     1 - (2 - 3)     1 + (2 + 3)     (a < b) < c     (if a then 1 else 2) + 3
void expr_print(const Expr *e, FILE *out);

// Calcula a op b. Si puede, guarda el valor en *result y devuelve 1. En una división entre un
// divisor igual a 0, devuelve 0 y no toca *result. Una comparación vale 1 o 0.
int expr_apply(BinaryOp op, double a, double b, double *result);

// Calcula el valor de e, sin funciones. Si puede, lo guarda en *result y devuelve 1. Si hay una
// variable que no existe en env, una división entre cero o una llamada, devuelve 0 y no toca
// *result. El intérprete no la usa: interp.c tiene su propio eval, que conoce las funciones.
int expr_eval(const Expr *e, const Env *env, double *result);

// Devuelve una copia independiente del árbol. NULL si no hay memoria, sin dejar nada reservado.
// Con NULL devuelve NULL.
Expr *expr_copy(const Expr *e);

// Devuelve 1 si los dos árboles tienen la misma forma y los mismos valores, y 0 si no.
int expr_equal(const Expr *a, const Expr *b);

// Se queda con e y devuelve el árbol simplificado. Libera todo lo que ya no se usa. Simplifica
// primero a los hijos, y luego aplica a cada nodo estas reglas:
//   x + 0, 0 + x, x - 0, x * 1, 1 * x, x / 1  ->  x
//   x * 0, 0 * x                              ->  0
//   dos números                               ->  el resultado, salvo si es una división entre 0
// En un if y en una llamada solo simplifica a los hijos. Con NULL devuelve NULL.
Expr *expr_simplify(Expr *e);

// Devuelve un árbol nuevo con la derivada de e respecto de la variable name; e no cambia. Solo
// cubre + - * /: con una comparación, un if o una llamada, o si no hay memoria, devuelve NULL.
Expr *expr_derive(const Expr *e, const char *name);

#endif
lab/interp.cDescargar lab/interp.c
#include "interp.h"

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

// Una entrada de la tabla de funciones. Toma prestado el def: su owner es el programa,
// que vive más que la tabla.
typedef struct function {
    const Stmt *definition;
    struct function *next;
} Function;

// El contexto del intérprete. Va por parámetro: no hay variables globales en C.
typedef struct {
    Env *global;            // el marco global de Mini
    Function *functions;    // la tabla de funciones de Mini
    FILE *out;
    RunError *error;
} Interp;

static int eval(Interp *in, const Expr *e, const Env *frame, double *result);

// Anota un error de ejecución y devuelve 0, para poder escribir: return run_fail(...);
static int run_fail(Interp *in, const char *what, const char *name) {
    snprintf(in->error->message, sizeof in->error->message, "%s: %.64s", what, name);
    return 0;
}

static int no_memory(Interp *in) {
    snprintf(in->error->message, sizeof in->error->message, "no hay memoria");
    return 0;
}

static int wrong_arg_count(Interp *in, const char *name, int expected, int given) {
    snprintf(in->error->message, sizeof in->error->message,
             "número de argumentos incorrecto: %.64s espera %d y recibió %d", name, expected, given);
    return 0;
}

// Devuelve el def de esa función, o NULL si no se ha ejecutado ninguno con ese nombre.
static const Stmt *find_function(const Interp *in, const char *name) {
    for (const Function *f = in->functions; f != NULL; f = f->next) {
        if (strcmp(f->definition->as.def.name, name) == 0) {
            return f->definition;
        }
    }
    return NULL;
}

// Agrega la función a la tabla. Si el nombre ya estaba, el def nuevo reemplaza al anterior.
static int define_function(Interp *in, const Stmt *definition) {
    for (Function *f = in->functions; f != NULL; f = f->next) {
        if (strcmp(f->definition->as.def.name, definition->as.def.name) == 0) {
            f->definition = definition;
            return 1;
        }
    }
    Function *f = malloc(sizeof *f);
    if (f == NULL) {
        return no_memory(in);
    }
    f->definition = definition;
    f->next = in->functions;
    in->functions = f;
    return 1;
}

static void free_functions(Function *functions) {
    while (functions != NULL) {
        Function *next = functions->next;
        free(functions);
        functions = next;
    }
}

static int eval_binary(Interp *in, const Expr *e, const Env *frame, double *result) {
    double a, b;
    if (!eval(in, e->as.binary.left, frame, &a) || !eval(in, e->as.binary.right, frame, &b)) {
        return 0;
    }
    if (!expr_apply(e->as.binary.op, a, b, result)) {
        snprintf(in->error->message, sizeof in->error->message, "división entre cero");
        return 0;
    }
    return 1;
}

// Evalúa la condición, y luego solo la rama elegida.
static int eval_if(Interp *in, const Expr *e, const Env *frame, double *result) {
    // --- Por escribir. ---
    (void) e;
    (void) frame;
    (void) result;
    return run_fail(in, "falta escribir", "eval_if");
}

// Evalúa una llamada: el modelo de entornos de la semana 3.
static int eval_call(Interp *in, const Expr *e, const Env *frame, double *result) {
    const char *name = e->as.call.name;
    const Stmt *function = find_function(in, name);
    if (function == NULL) {
        return run_fail(in, "función desconocida", name);
    }
    int count = e->as.call.arg_count;
    if (count != function->as.def.param_count) {
        return wrong_arg_count(in, name, function->as.def.param_count, count);
    }
    // --- Por escribir, en este orden:
    // 1. Evaluar cada argumento en frame, el marco de quien llama, y guardar los valores en un arreglo
    //    de double con malloc. Si count es 0, no reserven nada: malloc(0) puede devolver NULL.
    // 2. Crear el marco nuevo con env_new(in->global).
    // 3. Definir cada parámetro con env_set, y liberar el arreglo.
    // 4. Evaluar function->as.def.body en el marco nuevo, liberar el marco y devolver el resultado.
    // Cada camino de error libera lo que ya reservó. ---
    (void) frame;
    (void) result;
    return run_fail(in, "falta terminar eval_call", name);
}

static int eval(Interp *in, const Expr *e, const Env *frame, double *result) {
    switch (e->kind) {
    case EXPR_NUMBER:
        *result = e->as.number;
        return 1;
    case EXPR_VARIABLE:
        if (!env_get(frame, e->as.name, result)) {
            return run_fail(in, "variable desconocida", e->as.name);
        }
        return 1;
    case EXPR_BINARY:
        return eval_binary(in, e, frame, result);
    case EXPR_IF:
        return eval_if(in, e, frame, result);
    case EXPR_CALL:
        return eval_call(in, e, frame, result);
    }
    return 0;
}

// Ejecuta una instrucción. Las instrucciones siempre corren en el marco global.
static int exec_statement(Interp *in, const Stmt *s) {
    switch (s->kind) {
    case STMT_LET:
        // --- Por escribir: evaluar en in->global, y guardar con env_set. ---
        return run_fail(in, "falta escribir", "let");
    case STMT_PRINT:
        // --- Por escribir: evaluar en in->global, y escribir en in->out con "%g\n". ---
        return run_fail(in, "falta escribir", "print");
    case STMT_DEF:
        return define_function(in, s);
    }
    return 0;
}

int run_program(const Stmt *program, FILE *out, RunError *error) {
    error->message[0] = '\0';
    Interp in = {.global = env_new(NULL), .functions = NULL, .out = out, .error = error};
    if (in.global == NULL) {
        return no_memory(&in);
    }
    int ok = 1;
    for (const Stmt *s = program; s != NULL && ok; s = s->next) {
        ok = exec_statement(&in, s);
    }
    free_functions(in.functions);
    env_free(in.global);
    return ok;
}
lab/interp.hDescargar lab/interp.h
// El intérprete de Mini: corre un programa ya analizado.
#ifndef INTERP_H
#define INTERP_H

#include <stdio.h>

#include "stmt.h"

typedef struct {
    char message[128];
} RunError;

// Corre las instrucciones en orden y escribe en out lo que manden los print. Devuelve 1 si terminó
// bien, y 0 con error lleno si hubo un error de ejecución. No se apropia del programa, y no deja
// nada reservado al volver.
int run_program(const Stmt *program, FILE *out, RunError *error);

#endif
lab/lexer.cDescargar lab/lexer.c
#include "lexer.h"

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

struct lexer {
    const char *current;    // el siguiente carácter por leer
    int line;
    int column;
};

Lexer *lexer_new(const char *source) {
    Lexer *lexer = malloc(sizeof *lexer);
    if (lexer == NULL) {
        return NULL;
    }
    lexer->current = source;
    lexer->line = 1;
    lexer->column = 1;
    return lexer;
}

void lexer_free(Lexer *lexer) {
    free(lexer);
}

// Consume un carácter y lleva la cuenta de la línea y la columna.
static void advance(Lexer *lexer) {
    if (*lexer->current == '\n') {
        lexer->line++;
        lexer->column = 1;
    } else {
        lexer->column++;
    }
    lexer->current++;
}

// Salta lo que separa a los tokens: espacios y comentarios.
static void skip_blanks(Lexer *lexer) {
    for (;;) {
        if (isspace((unsigned char) *lexer->current)) {
            advance(lexer);
        } else if (*lexer->current == '#') {
            while (*lexer->current != '\0' && *lexer->current != '\n') {
                advance(lexer);
            }
        } else {
            return;
        }
    }
}

static int starts_name(char c) {
    return isalpha((unsigned char) c) || c == '_';
}

static int continues_name(char c) {
    return isalnum((unsigned char) c) || c == '_';
}

static int is_digit(char c) {
    return isdigit((unsigned char) c);
}

// Un token que ya se reconoció: guarda su tipo y su texto.
static Token finish(Token token, TokenKind kind, const char *text) {
    token.kind = kind;
    snprintf(token.text, sizeof token.text, "%s", text);
    return token;
}

// Lee desde start hasta el lexer actual y lo copia a token->text.
// Devuelve 0 si no cabe. Mide primero: no se escribe nada fuera del arreglo.
static int copy_text(Token *token, const char *start, const Lexer *lexer) {
    size_t length = (size_t) (lexer->current - start);
    if (length >= sizeof token->text) {
        return 0;
    }
    memcpy(token->text, start, length);
    token->text[length] = '\0';
    return 1;
}

// NUMBER: dígitos y una parte decimal opcional. El punto solo entra si le sigue un dígito.
static Token read_number(Lexer *lexer, Token token) {
    const char *start = lexer->current;
    while (is_digit(*lexer->current)) {
        advance(lexer);
    }
    if (lexer->current[0] == '.' && is_digit(lexer->current[1])) {
        advance(lexer);
        while (is_digit(*lexer->current)) {
            advance(lexer);
        }
    }
    if (!copy_text(&token, start, lexer)) {
        return finish(token, TOKEN_ERROR, "número demasiado largo");
    }
    token.kind = TOKEN_NUMBER;
    token.number = strtod(token.text, NULL);
    return token;
}

// Una palabra reservada, o TOKEN_NAME si no es ninguna.
static TokenKind keyword_or_name(const char *text) {
    static const struct {
        char word[8];
        TokenKind kind;
    } keywords[] = {
        {"let", TOKEN_LET}, {"print", TOKEN_PRINT}, {"def", TOKEN_DEF},
        {"if", TOKEN_IF}, {"then", TOKEN_THEN}, {"else", TOKEN_ELSE},
    };
    size_t count = sizeof keywords / sizeof keywords[0];
    for (size_t i = 0; i < count; i++) {
        if (strcmp(text, keywords[i].word) == 0) {
            return keywords[i].kind;
        }
    }
    return TOKEN_NAME;
}

// NAME: letra o guion bajo, y luego letras, dígitos o guiones bajos.
static Token read_name(Lexer *lexer, Token token) {
    const char *start = lexer->current;
    while (continues_name(*lexer->current)) {
        advance(lexer);
    }
    if (!copy_text(&token, start, lexer)) {
        return finish(token, TOKEN_ERROR, "nombre demasiado largo");
    }
    token.kind = keyword_or_name(token.text);
    return token;
}

// Consume c si es el carácter que sigue, y devuelve 1 si lo consumió.
static int match(Lexer *lexer, char c) {
    if (*lexer->current != c) {
        return 0;
    }
    advance(lexer);
    return 1;
}

// Un carácter que no empieza ningún token. Lo consume, para que se pueda seguir.
static Token unexpected(Lexer *lexer, Token token) {
    unsigned char c = (unsigned char) *lexer->current;
    advance(lexer);
    token.kind = TOKEN_ERROR;
    if (isprint(c)) {
        snprintf(token.text, sizeof token.text, "carácter inesperado '%c'", c);
    } else {
        snprintf(token.text, sizeof token.text, "carácter inesperado (byte %d)", c);
    }
    return token;
}

Token lexer_next(Lexer *lexer) {
    skip_blanks(lexer);

    Token token = {.kind = TOKEN_END, .number = 0.0, .text = "", .line = lexer->line, .column = lexer->column};
    char c = *lexer->current;
    if (c == '\0') {
        return token;
    }
    if (is_digit(c)) {
        return read_number(lexer, token);
    }
    if (starts_name(c)) {
        return read_name(lexer, token);
    }

    switch (c) {
        case '+': advance(lexer); return finish(token, TOKEN_PLUS, "+");
        case '-': advance(lexer); return finish(token, TOKEN_MINUS, "-");
        case '*': advance(lexer); return finish(token, TOKEN_STAR, "*");
        case '/': advance(lexer); return finish(token, TOKEN_SLASH, "/");
        case '(': advance(lexer); return finish(token, TOKEN_LPAREN, "(");
        case ')': advance(lexer); return finish(token, TOKEN_RPAREN, ")");
        case ',': advance(lexer); return finish(token, TOKEN_COMMA, ",");
        case '=':
            advance(lexer);
            return match(lexer, '=') ? finish(token, TOKEN_EQUAL, "==") : finish(token, TOKEN_ASSIGN, "=");
        case '<':
            advance(lexer);
            return match(lexer, '=') ? finish(token, TOKEN_LESS_EQUAL, "<=") : finish(token, TOKEN_LESS, "<");
        case '>':
            advance(lexer);
            return match(lexer, '=') ? finish(token, TOKEN_GREATER_EQUAL, ">=") : finish(token, TOKEN_GREATER, ">");
        case '!':
            if (lexer->current[1] == '=') {
                advance(lexer);
                advance(lexer);
                return finish(token, TOKEN_NOT_EQUAL, "!=");
            }
            return unexpected(lexer, token);
        default: return unexpected(lexer, token);
    }
}
lab/lexer.hDescargar lab/lexer.h
// Análisis léxico del lenguaje Mini: parte un texto en tokens, uno por llamada.
//
// Owner: quien llama a lexer_new libera con lexer_free. El lexer no copia el texto:
// source debe seguir vivo mientras se use el lexer.
#ifndef LEXER_H
#define LEXER_H

typedef enum {
    TOKEN_NUMBER, TOKEN_NAME,
    TOKEN_LET, TOKEN_PRINT, TOKEN_DEF, TOKEN_IF, TOKEN_THEN, TOKEN_ELSE,
    TOKEN_PLUS, TOKEN_MINUS, TOKEN_STAR, TOKEN_SLASH,
    TOKEN_LPAREN, TOKEN_RPAREN, TOKEN_COMMA, TOKEN_ASSIGN,
    TOKEN_LESS, TOKEN_LESS_EQUAL, TOKEN_GREATER, TOKEN_GREATER_EQUAL, TOKEN_EQUAL, TOKEN_NOT_EQUAL,
    TOKEN_END, TOKEN_ERROR
} TokenKind;

// line y column empiezan en 1 y dicen dónde empieza el token. column cuenta bytes.
// text es el texto del token. En un TOKEN_ERROR, text es el mensaje de error.
// number solo tiene sentido en un TOKEN_NUMBER.
typedef struct {
    TokenKind kind;
    double number;
    char text[64];
    int line;
    int column;
} Token;

typedef struct lexer Lexer;

// Devuelve NULL si no hay memoria.
Lexer *lexer_new(const char *source);

// Acepta NULL.
void lexer_free(Lexer *lexer);

// Devuelve el token siguiente. Al llegar al final devuelve TOKEN_END, y lo sigue
// devolviendo si se le vuelve a llamar. Después de un TOKEN_ERROR se puede seguir.
Token lexer_next(Lexer *lexer);

#endif
lab/mini.cDescargar lab/mini.c
// Corre un programa de Mini desde un archivo: mini archivo.mini
#include <stdio.h>
#include <stdlib.h>

#include "interp.h"
#include "parser.h"

// Lee el archivo completo a un bloque nuevo, terminado en '\0'. Quien llama lo libera.
// Devuelve NULL si no pudo abrirlo o si no hay memoria.
static char *read_file(const char *path) {
    FILE *file = fopen(path, "r");
    if (file == NULL) {
        return NULL;
    }
    size_t capacity = 1024;
    size_t length = 0;
    char *text = malloc(capacity);
    while (text != NULL) {
        length += fread(text + length, 1, capacity - length - 1, file);
        if (length < capacity - 1) {
            text[length] = '\0';
            break;
        }
        capacity *= 2;
        char *grown = realloc(text, capacity);
        if (grown == NULL) {
            free(text);
        }
        text = grown;
    }
    fclose(file);
    return text;
}

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "uso: %s archivo.mini\n", argv[0]);
        return 2;
    }
    char *source = read_file(argv[1]);
    if (source == NULL) {
        fprintf(stderr, "%s: no se pudo leer el archivo\n", argv[1]);
        return 2;
    }
    ParseError syntax;
    Stmt *program = parse_program(source, &syntax);
    int status = 0;
    if (program == NULL && syntax.message[0] != '\0') {
        fprintf(stderr, "%s:%d:%d: error de sintaxis: %s\n", argv[1], syntax.line, syntax.column, syntax.message);
        status = 1;
    } else {
        RunError error;
        if (!run_program(program, stdout, &error)) {
            // Primero lo que ya escribieron los print, para que salga en orden.
            fflush(stdout);
            fprintf(stderr, "%s: error de ejecución: %s\n", argv[1], error.message);
            status = 1;
        }
    }
    stmt_free(program);
    free(source);
    return status;
}
lab/parser.cDescargar lab/parser.c
// Parser descendente recursivo: una función por cada regla de la gramática.
//
//   program   → statement*
//   statement → "let" NAME "=" expr
//             | "print" expr
//             | "def" NAME "(" [ NAME ("," NAME)* ] ")" "=" expr
//   expr      → "if" expr "then" expr "else" expr | compare
//   compare   → sum [ ("<" | "<=" | ">" | ">=" | "==" | "!=") sum ]
//   sum       → term (("+" | "-") term)*
//   term      → unary (("*" | "/") unary)*
//   unary     → "-" unary | primary
//   primary   → NUMBER | NAME | NAME "(" [ expr ("," expr)* ] ")" | "(" expr ")"
//
// Cada parse_* mira p->current, consume lo que le toca y devuelve un árbol del que quien
// llama es owner. Si falla, deja el error llenado, libera lo suyo y devuelve NULL.
#include "parser.h"

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

#include "lexer.h"

typedef struct {
    Lexer *lexer;
    Token current;      // el token que toca mirar, todavía sin consumir
    ParseError *error;
} Parser;

static Expr *parse_expr(Parser *p);

// Pasa al token siguiente.
static void advance(Parser *p) {
    p->current = lexer_next(p->lexer);
}

static void syntax_error(Parser *p, Token token, const char *message) {
    p->error->line = token.line;
    p->error->column = token.column;
    const char *text = token.kind == TOKEN_ERROR ? token.text : message;
    snprintf(p->error->message, sizeof p->error->message, "%s", text);
}

static void no_memory(Parser *p) {
    syntax_error(p, p->current, "no hay memoria");
}

// Si el token actual es de ese tipo, lo consume y devuelve 1. Si no, anota el error y devuelve 0.
static int expect(Parser *p, TokenKind kind, const char *message) {
    if (p->current.kind != kind) {
        syntax_error(p, p->current, message);
        return 0;
    }
    advance(p);
    return 1;
}

// Combina dos árboles. expr_binary ya libera a sus hijos si no hay memoria.
static Expr *make_binary(Parser *p, BinaryOp op, Expr *left, Expr *right) {
    Expr *node = expr_binary(op, left, right);
    if (node == NULL) {
        syntax_error(p, p->current, "no hay memoria");
    }
    return node;
}

// if → "if" expr "then" expr "else" expr
static Expr *parse_if(Parser *p) {
    // --- Por escribir. El token actual es "if". ---
    // Si algo falla a la mitad, liberen lo que ya tengan y devuelvan NULL.
    syntax_error(p, p->current, "falta escribir parse_if");
    return NULL;
}

// call → NAME "(" [ expr ("," expr)* ] ")". El nombre ya se consumió, y el token actual es "(".
static Expr *parse_call(Parser *p, const char *name) {
    // --- Por escribir. Armen un arreglo Expr ** que crece con realloc, como en parse_parameters.
    // Si no hay argumentos, el arreglo queda en NULL. Si algo falla, liberen cada Expr ya leído y
    // el arreglo. Al final, expr_call se vuelve owner de todo. ---
    (void) name;
    syntax_error(p, p->current, "falta escribir parse_call");
    return NULL;
}

static Expr *parse_primary(Parser *p) {
    Token token = p->current;
    if (token.kind == TOKEN_NUMBER || token.kind == TOKEN_NAME) {
        advance(p);
        if (token.kind == TOKEN_NAME && p->current.kind == TOKEN_LPAREN) {
            return parse_call(p, token.text);
        }
        Expr *leaf = token.kind == TOKEN_NUMBER ? expr_number(token.number) : expr_variable(token.text);
        if (leaf == NULL) {
            syntax_error(p, token, "no hay memoria");
        }
        return leaf;
    }
    if (token.kind == TOKEN_LPAREN) {
        advance(p);
        Expr *inside = parse_expr(p);
        if (inside == NULL) {
            return NULL;
        }
        if (p->current.kind != TOKEN_RPAREN) {
            syntax_error(p, p->current, "se esperaba ')'");
            expr_free(inside);
            return NULL;
        }
        advance(p);
        return inside;
    }
    syntax_error(p, token, "se esperaba un número, un nombre o '('");
    return NULL;
}

static Expr *parse_unary(Parser *p) {
    if (p->current.kind != TOKEN_MINUS) {
        return parse_primary(p);
    }
    advance(p);
    Expr *operand = parse_unary(p);
    if (operand == NULL) {
        return NULL;
    }
    Expr *zero = expr_number(0.0);
    if (zero == NULL) {
        expr_free(operand);
        syntax_error(p, p->current, "no hay memoria");
        return NULL;
    }
    return make_binary(p, OP_SUB, zero, operand);
}

static Expr *parse_term(Parser *p) {
    Expr *left = parse_unary(p);
    while (left != NULL && (p->current.kind == TOKEN_STAR || p->current.kind == TOKEN_SLASH)) {
        BinaryOp op = p->current.kind == TOKEN_STAR ? OP_MUL : OP_DIV;
        advance(p);
        Expr *right = parse_unary(p);
        if (right == NULL) {
            expr_free(left);
            return NULL;
        }
        left = make_binary(p, op, left, right);
    }
    return left;
}

static Expr *parse_sum(Parser *p) {
    Expr *left = parse_term(p);
    while (left != NULL && (p->current.kind == TOKEN_PLUS || p->current.kind == TOKEN_MINUS)) {
        BinaryOp op = p->current.kind == TOKEN_PLUS ? OP_ADD : OP_SUB;
        advance(p);
        Expr *right = parse_term(p);
        if (right == NULL) {
            expr_free(left);
            return NULL;
        }
        left = make_binary(p, op, left, right);
    }
    return left;
}

// Si el token es un comparador, guarda su operación y devuelve 1.
static int comparison_op(TokenKind kind, BinaryOp *op) {
    switch (kind) {
        case TOKEN_LESS: *op = OP_LESS; return 1;
        case TOKEN_LESS_EQUAL: *op = OP_LESS_EQUAL; return 1;
        case TOKEN_GREATER: *op = OP_GREATER; return 1;
        case TOKEN_GREATER_EQUAL: *op = OP_GREATER_EQUAL; return 1;
        case TOKEN_EQUAL: *op = OP_EQUAL; return 1;
        case TOKEN_NOT_EQUAL: *op = OP_NOT_EQUAL; return 1;
        default: return 0;
    }
}

static Expr *parse_compare(Parser *p) {
    Expr *left = parse_sum(p);
    BinaryOp op;
    if (left == NULL || !comparison_op(p->current.kind, &op)) {
        return left;
    }
    advance(p);
    Expr *right = parse_sum(p);
    if (right == NULL) {
        expr_free(left);
        return NULL;
    }
    return make_binary(p, op, left, right);
}

static Expr *parse_expr(Parser *p) {
    if (p->current.kind == TOKEN_IF) {
        return parse_if(p);
    }
    return parse_compare(p);
}

// Lee los parámetros de un def, hasta el ")" incluido. El "(" ya se consumió.
// Devuelve 1 y entrega el arreglo, que es NULL si son 0. Si falla, no deja nada reservado.
static int parse_parameters(Parser *p, char ***params, int *param_count) {
    char **names = NULL;
    int count = 0;
    int capacity = 0;
    int more = p->current.kind != TOKEN_RPAREN;
    while (more) {
        if (p->current.kind != TOKEN_NAME) {
            syntax_error(p, p->current, "se esperaba el nombre de un parámetro");
            free_names(names, count);
            return 0;
        }
        if (count == capacity) {
            int new_capacity = capacity == 0 ? 4 : capacity * 2;
            char **grown = realloc(names, (size_t) new_capacity * sizeof *grown);
            if (grown == NULL) {
                no_memory(p);
                free_names(names, count);
                return 0;
            }
            names = grown;
            capacity = new_capacity;
        }
        names[count] = malloc(strlen(p->current.text) + 1);
        if (names[count] == NULL) {
            no_memory(p);
            free_names(names, count);
            return 0;
        }
        strcpy(names[count], p->current.text);
        count++;
        advance(p);
        more = p->current.kind == TOKEN_COMMA;
        if (more) {
            advance(p);
        }
    }
    if (!expect(p, TOKEN_RPAREN, "se esperaba ')'")) {
        free_names(names, count);
        return 0;
    }
    *params = names;
    *param_count = count;
    return 1;
}

// "let" NAME "=" expr
static Stmt *parse_let(Parser *p) {
    // --- Por escribir. Usen expect para los tokens fijos, y guarden el token del nombre antes de
    // pasarlo. Ojo: guardar un Token copia su text. ---
    syntax_error(p, p->current, "falta escribir parse_let");
    return NULL;
}

// "print" expr
static Stmt *parse_print(Parser *p) {
    // --- Por escribir. ---
    syntax_error(p, p->current, "falta escribir parse_print");
    return NULL;
}

// "def" NAME "(" [ NAME ("," NAME)* ] ")" "=" expr
static Stmt *parse_def(Parser *p) {
    advance(p);
    Token name = p->current;
    if (!expect(p, TOKEN_NAME, "se esperaba un nombre después de 'def'")) {
        return NULL;
    }
    if (!expect(p, TOKEN_LPAREN, "se esperaba '('")) {
        return NULL;
    }
    char **params;
    int param_count;
    if (!parse_parameters(p, &params, &param_count)) {
        return NULL;
    }
    // --- Por escribir: el "=", el cuerpo y stmt_def. Desde aquí, params ya es de ustedes: si algo
    // falla, se libera con free_names. Si stmt_def recibe params, se vuelve su owner. ---
    (void) name;
    free_names(params, param_count);
    syntax_error(p, p->current, "falta terminar parse_def");
    return NULL;
}

// statement → let | print | def. El primer token decide cuál es.
static Stmt *parse_statement(Parser *p) {
    switch (p->current.kind) {
    case TOKEN_LET:
        return parse_let(p);
    case TOKEN_PRINT:
        return parse_print(p);
    case TOKEN_DEF:
        return parse_def(p);
    default:
        syntax_error(p, p->current, "se esperaba 'let', 'print' o 'def'");
        return NULL;
    }
}

// Prepara el parser y lee el primer token. Devuelve 0 si no hay memoria.
static int open_parser(Parser *p, const char *source, ParseError *error) {
    error->line = 0;
    error->column = 0;
    error->message[0] = '\0';
    p->error = error;
    p->lexer = lexer_new(source);
    if (p->lexer == NULL) {
        snprintf(error->message, sizeof error->message, "no hay memoria");
        return 0;
    }
    advance(p);
    return 1;
}

Expr *parse_expression(const char *source, ParseError *error) {
    Parser p;
    if (!open_parser(&p, source, error)) {
        return NULL;
    }
    Expr *e = parse_expr(&p);
    if (e != NULL && p.current.kind != TOKEN_END) {
        syntax_error(&p, p.current, "sobra texto después de la expresión");
        expr_free(e);
        e = NULL;
    }
    lexer_free(p.lexer);
    return e;
}

Stmt *parse_program(const char *source, ParseError *error) {
    Parser p;
    if (!open_parser(&p, source, error)) {
        return NULL;
    }
    Stmt *first = NULL;
    Stmt **tail = &first;   // dónde se engancha la próxima instrucción
    while (p.current.kind != TOKEN_END) {
        Stmt *s = parse_statement(&p);
        if (s == NULL) {
            stmt_free(first);
            lexer_free(p.lexer);
            return NULL;
        }
        *tail = s;
        tail = &s->next;
    }
    lexer_free(p.lexer);
    return first;
}
lab/parser.hDescargar lab/parser.h
// Análisis sintáctico de Mini: de texto a árbol.
#ifndef PARSER_H
#define PARSER_H

#include "stmt.h"

// line y column dicen dónde está el token con el que falló.
typedef struct {
    int line;
    int column;
    char message[128];
} ParseError;

// Lee una sola expresión. Devuelve el árbol, y quien llama es su owner.
// Devuelve NULL si hay un error de sintaxis, con error lleno y sin fugas.
Expr *parse_expression(const char *source, ParseError *error);

// Lee un programa completo. Devuelve la lista de instrucciones, y quien llama la libera con stmt_free.
// Si hay un error de sintaxis, devuelve NULL con error->message lleno y sin fugas. Un programa sin
// instrucciones es la lista vacía: también es NULL, pero con error->message vacío ("").
Stmt *parse_program(const char *source, ParseError *error);

#endif
lab/README.mdDescargar lab/README.md
# Laboratorio de la semana 15: terminar el intérprete

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

Lo que escribieron en las semanas 13 y 14 ya viene hecho, y ampliado para las variantes nuevas. Su propio código funciona, pero
así todos empiezan desde el mismo punto y el evaluador no carga con un bug de otra semana.

| Archivo | Qué es | ¿Lo cambian? |
|---|---|---|
| `env.h`, `env.c` | La tabla de variables de cada marco. Se usa solo por su encabezado | No |
| `expr.h`, `expr.c` | El árbol de la semana 13, con `EXPR_IF` y `EXPR_CALL`, y sus constructores | No |
| `lexer.h`, `lexer.c` | El análisis léxico de la semana 14, completo | No |
| `stmt.h`, `stmt.c` | Las instrucciones: `Stmt`, sus constructores y `stmt_free` | No |
| `parser.h` | La interfaz del parser | No |
| `parser.c` | Las expresiones ya están. Faltan `parse_if`, `parse_call`, `parse_let`, `parse_print` y el final de `parse_def` | **Sí** |
| `interp.h` | La interfaz del intérprete | No |
| `interp.c` | La tabla de funciones y lo aritmético ya están. Faltan `eval_if`, el final de `eval_call` y dos casos de `exec_statement` | **Sí** |
| `mini.c` | El programa: lee un archivo, lo analiza y lo corre | No |
| `programs/` | Programas de Mini de ejemplo, también los que deben fallar | No |
| `mine/` | Sus propios programas de Mini, con el resultado esperado | **Sí** |

```sh
make lab                         # compila el intérprete en build/lab/mini
build/lab/mini lab/programs/fact.mini
make test-lab                    # comprueba todo el laboratorio; falla hasta que terminen
```

Se puede correr un grupo de pruebas solo:

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

Todo se compila con ASan y UBSan. Una fuga también hace fallar la prueba, aunque el resultado esté bien.

## Sesión 1: unir las partes

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

Lean `stmt.h`, `parse_parameters` y `parse_program` en `parser.c`, y luego `interp.c` hasta `eval`. Respondan por escrito:

1. En `run_program`, ¿quién es owner del marco global? ¿Y de cada `Stmt`?
2. Una entrada de `Function` guarda un `const Stmt *`. ¿Por qué no se libera ese `Stmt` en `free_functions`?
3. `Interp` va por parámetro a todas las funciones. ¿De qué semana es esa idea, y qué evita?
4. Si `parse_program` devuelve `NULL`, ¿cómo se distingue un error de sintaxis de un programa vacío?

### Parte 2: `let` y `print` (unos 15 minutos)

En `parser.c`, escriban `parse_let` y `parse_print`. En `interp.c`, los casos `STMT_LET` y `STMT_PRINT` de `exec_statement`.
Con eso, el intérprete ya corre programas desde un archivo. Creen uno propio con un `let` y un `print`, y córranlo.

```sh
make lab
LAB_DIR=lab python3 -m unittest test_lab.LetAndPrint
```

### Parte 3: `if` (unos 15 minutos)

Escriban `parse_if` y `eval_if`. El parser debe liberar lo que ya leyó si algo falla a la mitad. `eval_if` solo evalúa la rama
elegida, y eso es parte del lenguaje, no una optimización.

```sh
make lab
LAB_DIR=lab python3 -m unittest test_lab.IfExpression
```

### Parte 4: `def` y llamadas (unos 45 minutos)

1. Terminen `parse_def`, y escriban `parse_call` con un arreglo que crece, como en `parse_parameters`.
2. Antes de escribir `eval_call`, dibujen en papel los marcos de `fact(3)`, en el formato de la semana 3.
3. Terminen `eval_call`, siguiendo los cuatro pasos de su comentario. **El marco nuevo cuelga del global.**

```sh
make lab
build/lab/mini lab/programs/fact.mini
build/lab/mini lab/programs/fib.mini
LAB_DIR=lab python3 -m unittest test_lab.Calls
```

**Si no alcanza el tiempo:** lo que falte de `eval_call` lo terminan antes de la sesión 2, que empieza dándolo por hecho.

## Sesión 2: probar y repasar

### Parte 5: el conjunto de pruebas (unos 25 minutos)

```sh
make test-lab
```

Vayan grupo por grupo: `Programs`, `RunErrors`, `SyntaxErrors` e `Interface`. Si hay una fuga, el informe de ASan dice en qué
línea se reservó ese bloque. La pregunta es siempre la misma: ¿qué camino de error salió de la función sin liberarlo? También corran,
y lean, lo que pasa con los programas de `lab/programs` que deben fallar.

```sh
build/lab/mini lab/programs/caller_local.mini
build/lab/mini lab/programs/wrong_args.mini
build/lab/mini lab/programs/runaway.mini
```

### Parte 6: su programa, y una prueba que encuentre un bug (unos 30 minutos)

1. Escriban en `lab/mine/` un programa propio de Mini que haga algo que les interese, con su resultado en un `.expected` del mismo
   nombre. `example.mini` es el modelo. `make test-lab` corre todo lo que haya en esa carpeta.
2. Ahora introduzcan un bug a propósito en `interp.c`, uno por vez. El capítulo trabaja un ejemplo, `env_new((Env *) frame)`. Con estos
   otros dos, escriban en `lab/mine/` un programa que lo atrape, y que dé bien sin el bug. Antes, comprueben que `fact.mini` y `fib.mini` no
   lo atrapan:
   - `eval_call` define los parámetros al revés: `params[count - 1 - i]`.
   - `eval_if` trata como falso a todo lo que no sea positivo: `condition > 0`.
3. Corran su programa con el bug: debe fallar. Deshagan cada cambio, y comprueben que vuelve a pasar. Si encontraron un bug
   real de su intérprete en el camino, mejor: déjenlo anotado en un comentario del programa que lo atrapó.

### Parte 7: repaso (unos 35 minutos)

Sin mirar los apuntes, respondan las preguntas de repaso de la guía de la semana 15. Luego tomen la tabla "Qué idea sirvió en
cada parte" de los apuntes, tápenla, y digan para cada parte del intérprete de qué semana salió, y señalen una línea de código
donde se ve. Terminen con los ejercicios que el capítulo marca como de repaso.

## Si sobra tiempo

1. Agreguen un límite de profundidad: un contador en `Interp` y un error de ejecución en vez de un stack overflow. ¿Qué
   valor le ponen, y por qué ese? ¿Hace falta otro cuidado si cambia el tamaño de la pila?
2. Permitan `print` de varios valores: `print a, b`. ¿Qué cambia en la gramática, en `Stmt` y en `stmt_free`?
3. Hagan que un `def` con dos parámetros del mismo nombre sea un error de sintaxis. Hoy, gana el último.
lab/stmt.cDescargar lab/stmt.c
#include "stmt.h"

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

static char *copy_string(const char *text) {
    char *copy = malloc(strlen(text) + 1);
    if (copy != NULL) {
        strcpy(copy, text);
    }
    return copy;
}

static Stmt *new_stmt(StmtKind kind) {
    Stmt *s = malloc(sizeof *s);
    if (s != NULL) {
        s->kind = kind;
        s->next = NULL;
    }
    return s;
}

Stmt *stmt_let(const char *name, Expr *value) {
    Stmt *s = value != NULL ? new_stmt(STMT_LET) : NULL;
    char *copy = s != NULL ? copy_string(name) : NULL;
    if (copy == NULL) {
        free(s);
        expr_free(value);
        return NULL;
    }
    s->as.let.name = copy;
    s->as.let.value = value;
    return s;
}

Stmt *stmt_print(Expr *value) {
    Stmt *s = value != NULL ? new_stmt(STMT_PRINT) : NULL;
    if (s == NULL) {
        expr_free(value);
        return NULL;
    }
    s->as.print.value = value;
    return s;
}

Stmt *stmt_def(const char *name, char **params, int param_count, Expr *body) {
    Stmt *s = body != NULL ? new_stmt(STMT_DEF) : NULL;
    char *copy = s != NULL ? copy_string(name) : NULL;
    if (copy == NULL) {
        free(s);
        free_names(params, param_count);
        expr_free(body);
        return NULL;
    }
    s->as.def.name = copy;
    s->as.def.params = params;
    s->as.def.param_count = param_count;
    s->as.def.body = body;
    return s;
}

void free_names(char **names, int count) {
    for (int i = 0; i < count; i++) {
        free(names[i]);
    }
    free(names);
}

static void free_one(Stmt *s) {
    switch (s->kind) {
    case STMT_LET:
        free(s->as.let.name);
        expr_free(s->as.let.value);
        break;
    case STMT_PRINT:
        expr_free(s->as.print.value);
        break;
    case STMT_DEF:
        free(s->as.def.name);
        free_names(s->as.def.params, s->as.def.param_count);
        expr_free(s->as.def.body);
        break;
    }
    free(s);
}

// Con un ciclo: un programa largo no debe gastar un marco por instrucción.
void stmt_free(Stmt *program) {
    while (program != NULL) {
        Stmt *next = program->next;
        free_one(program);
        program = next;
    }
}
lab/stmt.hDescargar lab/stmt.h
// Las instrucciones de Mini. Un programa es una lista enlazada de Stmt, por el campo next.
//
// Ownership: cada instrucción es owner de sus cadenas y de sus árboles. Quien tiene la cabeza
// de la lista la libera con stmt_free.
#ifndef STMT_H
#define STMT_H

#include "expr.h"

typedef enum { STMT_LET, STMT_PRINT, STMT_DEF } StmtKind;

typedef struct stmt Stmt;

struct stmt {
    StmtKind kind;
    union {
        struct { char *name; Expr *value; } let;
        struct { Expr *value; } print;
        struct { char *name; char **params; int param_count; Expr *body; } def;     // params es NULL si son 0
    } as;
    Stmt *next;
};

// Devuelven una instrucción nueva con next en NULL. Se vuelven owners de value, params y body
// aunque fallen: en ese caso los liberan y devuelven NULL. Un value o un body NULL cuenta como falla.
Stmt *stmt_let(const char *name, Expr *value);
Stmt *stmt_print(Expr *value);
Stmt *stmt_def(const char *name, char **params, int param_count, Expr *body);

// Libera un arreglo de cadenas, como params. Acepta NULL.
void free_names(char **names, int count);

// Libera la lista completa. Acepta NULL.
void stmt_free(Stmt *program);

#endif
lab/mine/example.expectedDescargar lab/mine/example.expected
7
lab/mine/example.miniDescargar lab/mine/example.mini
# Modelo: el resultado esperado está en example.expected, con el mismo nombre.
def max(a, b) = if a > b then a else b
print max(2, 7)
lab/programs/args_in_caller.miniDescargar lab/programs/args_in_caller.mini
def sub(a, b) = a - b
let a = 10
let b = 3
print sub(b, a)
lab/programs/before_def.miniDescargar lab/programs/before_def.mini
print 1
print twice(4)
def twice(n) = n * 2
lab/programs/big.miniDescargar lab/programs/big.mini
def fact(n) = if n < 2 then 1 else n * fact(n - 1)
print fact(6)
print fact(10)
print fact(13)
lab/programs/caller_local.miniDescargar lab/programs/caller_local.mini
# show no tiene n, y el marco de outer no le sirve.
def show() = n
def outer(n) = show()
print outer(7)
lab/programs/deep.miniDescargar lab/programs/deep.mini
def sum(n) = if n < 1 then 0 else n + sum(n - 1)
print sum(1000)
lab/programs/even_odd.miniDescargar lab/programs/even_odd.mini
# Cada función llama a la otra. Funciona porque, al llamarlas, ya se ejecutaron los dos def.
def even(n) = if n == 0 then 1 else odd(n - 1)
def odd(n) = if n == 0 then 0 else even(n - 1)
print even(10)
print even(7)
lab/programs/fact.miniDescargar lab/programs/fact.mini
def fact(n) = if n < 2 then 1 else n * fact(n - 1)
let x = 5
print fact(x)     # 120
lab/programs/fact3.miniDescargar lab/programs/fact3.mini
def fact(n) = if n < 2 then 1 else n * fact(n - 1)
print fact(3)
lab/programs/fib.miniDescargar lab/programs/fib.mini
# Fibonacci en recursión en árbol, como en la semana 10.
def fib(n) = if n < 2 then n else fib(n - 1) + fib(n - 2)
print fib(1)
print fib(10)
print fib(20)
lab/programs/globals.miniDescargar lab/programs/globals.mini
# El cuerpo lee la variable global cuando corre, no cuando se define.
let rate = 2
def scale(n) = n * rate
print scale(5)
let rate = 3
print scale(5)
lab/programs/my_if.miniDescargar lab/programs/my_if.mini
# Un if escrito como función: una llamada evalúa todos sus argumentos antes de entrar.
def my_if(c, a, b) = if c then a else b
print my_if(1, 10, 20)
def fact(n) = my_if(n < 2, 1, n * fact(n - 1))
print fact(3)
lab/programs/only_branch.miniDescargar lab/programs/only_branch.mini
# Solo se evalúa la rama elegida: la otra dividiría entre cero.
let d = 0
print if d == 0 then 0 else 1 / d
lab/programs/runaway.miniDescargar lab/programs/runaway.mini
def forever(n) = forever(n + 1)
print forever(0)
lab/programs/scale.miniDescargar lab/programs/scale.mini
let rate = 2
def scale(n) = n * rate
def outer(n) = scale(n + 1)
print outer(4)
lab/programs/shadow.miniDescargar lab/programs/shadow.mini
# Un parámetro puede llamarse igual que una variable global: la tapa, sin cambiarla.
let x = 10
def double(x) = x * 2
print double(3)
print x
lab/programs/syntax_error.miniDescargar lab/programs/syntax_error.mini
def add(a, b) = a + b
print add(1, 2
let x = 3
lab/programs/unknown_variable.miniDescargar lab/programs/unknown_variable.mini
let price = 3
print prcie * 2
lab/programs/wrong_args.miniDescargar lab/programs/wrong_args.mini
def add(a, b) = a + b
print add(1, 2)
print add(1)
lab/programs/zero_division.miniDescargar lab/programs/zero_division.mini
def inverse(n) = 1 / n
print inverse(4)
print inverse(0)

Compilación

Cómo se compilan y ejecutan los ejemplos.

MakefileDescargar Makefile
# Semana 15: el intérprete. Se usa dentro del entorno del curso.
#
#   make           compila los programas de la clase en build/
#   make lab       compila el intérprete del laboratorio en build/lab/
#   make test      corre las pruebas de los ejemplos y de los apuntes
#   make test-lab  prueba su laboratorio; falla hasta que lo terminen
#   make clean     borra lo generado

CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -g -fsanitize=address,undefined
BUILD = build

MINI_OBJECTS = env expr lexer stmt parser interp mini
LAB_HEADERS = lab/env.h lab/expr.h lab/lexer.h lab/stmt.h lab/parser.h lab/interp.h

.PHONY: all lab solution test test-lab clean

all: $(BUILD)/frames

# frames usa el módulo Env del laboratorio.
$(BUILD)/frames: frames.c lab/env.c lab/env.h | $(BUILD)
	$(CC) $(CFLAGS) -Ilab frames.c lab/env.c -o $@

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

$(BUILD)/lab/mini: $(MINI_OBJECTS:%=$(BUILD)/lab/%.o)
	$(CC) $(CFLAGS) $^ -o $@

# La solución trae solo lo que ustedes escriben. Lo demás se toma de lab/.
$(BUILD)/solution/%.o: solution/%.c $(LAB_HEADERS) | $(BUILD)/solution
	$(CC) $(CFLAGS) -Ilab -c $< -o $@

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

$(BUILD)/solution/mini: $(MINI_OBJECTS:%=$(BUILD)/solution/%.o)
	$(CC) $(CFLAGS) $^ -o $@

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

lab: $(BUILD)/lab/mini

solution: $(BUILD)/solution/mini

# Si existe solution/, también se comprueba la solución del laboratorio.
test: all
	@if [ -d solution ]; then $(MAKE) --no-print-directory solution; fi
	python3 -m unittest discover -p 'test_*.py'

test-lab: all lab
	LAB_DIR=lab python3 -m unittest -v test_lab

clean:
	rm -rf $(BUILD) __pycache__