# Laboratoire facultatif — Fonctions et portée lexicale

## Un programme peut fabriquer une fonction

En AP2, nous avons relié texte, tokens, arbre et évaluation. Nous allons
maintenant représenter une fonction comme une valeur, l'appeler, la passer
à une autre fonction et la renvoyer comme résultat.

Ce notebook est autonome. Le lexer, le parser et l'évaluateur du socle AP2
sont fournis ci-dessous. Il n'est pas nécessaire de copier votre rendu AP2.
Le code reconnaît déjà la syntaxe des fonctions ; votre travail concerne
les emplacements **AP3** de `MiniEval` et les expériences qui suivent.

Ce laboratoire prolonge AP2 et reste facultatif. En AP3, le socle de
compilation refuse les fonctions ; l'étape 05 facultative les compile sans
capture, et le typeur est devenu un projet d'AP9. Ici, nous restons dans
l'interpréteur. Choisissez un exercice selon vos besoins : E1 (35 min),
E2 (40 min), E3 (35 min), E4 (30 min).
Évaluez les briques fournies dans l'ordre. Après modification d'un module,
réévaluez sa cellule entière avant de relancer les vérifications.

Les nombres s'écrivent toujours avec un point, par ex. `0.0` ou `12.5`.
Le langage conserve le même contrat : quatre opérations, liaisons locales,
blocs, booléens et `case`. Le dernier résultat d'un bloc est sa valeur.


```elixir
defmodule ControleMini do
  def egal(obtenu, attendu) do
    if obtenu != attendu do
      raise "Attendu #{inspect(attendu)}, obtenu #{inspect(obtenu)}"
    end
    :ok
  end

  def proche({:ok, obtenu}, attendu) when is_float(obtenu) do
    if abs(obtenu - attendu) > 1.0e-9 do
      raise "Attendu environ #{attendu}, obtenu #{obtenu}"
    end
    :ok
  end

  def proche(obtenu, attendu) do
    raise "Attendu un succès numérique proche de #{attendu}, obtenu #{inspect(obtenu)}"
  end

  def erreur({:error, _raison}) do
    :ok
  end

  def erreur(obtenu) do
    raise "Une erreur explicite était attendue, obtenu #{inspect(obtenu)}"
  end

  def phase({:error, {phase, _raison}}, phase) do
    :ok
  end

  def phase({:error, {phase, _raison, _reste}}, phase) do
    :ok
  end

  def phase(obtenu, phase) do
    raise "Une erreur de #{phase} était attendue, obtenu #{inspect(obtenu)}"
  end
end
```

## Briques du socle AP2 — Code fourni

Évaluez ces modules tels quels. `MiniLiteral` lit les littéraux, `MiniLexer`
produit les tokens, `MiniParser` construit les arbres et `MiniEnv` recherche
une liaison. Les helpers utilisent quelques outils fournis (`with`, gardes,
expressions régulières) ; l'exercice porte sur les environnements et
l'évaluation des fonctions, pas sur la réécriture de ces outils.

```elixir
defmodule MiniLiteral do
  def read(text) do
    cond do
      Regex.match?(~r/^[0-9]/, text) -> number(text)
      Regex.match?(~r/^[A-Za-z_]/, text) -> name(text)
      true -> {:error, :unknown_symbol}
    end
  end

  defp number(text) do
    case Regex.run(~r/^[0-9]+\.[0-9]+(?![a-zA-Z0-9_.])/, text) do
      [literal] ->
        try do
          {:ok, {:number, String.to_float(literal)}, String.replace_prefix(text, literal, "")}
        rescue
          ArgumentError -> {:error, :number_out_of_range}
        end

      nil -> {:error, :expected_decimal}
    end
  end

  defp name(text) do
    [literal] = Regex.run(~r/^[A-Za-z_][A-Za-z0-9_]*/, text)
    rest = String.replace_prefix(text, literal, "")

    case keyword(literal) do
      {:ok, token} -> {:ok, token, rest}
      :name ->
        if Regex.match?(~r/^[a-z][a-z0-9_]*$/, literal) do
          {:ok, {:name, literal}, rest}
        else
          {:error, :invalid_name}
        end
    end
  end

  defp keyword("let"), do: {:ok, :let}
  defp keyword("case"), do: {:ok, :case}
  defp keyword("fn"), do: {:ok, :fn}
  defp keyword("True"), do: {:ok, {:bool, true}}
  defp keyword("False"), do: {:ok, {:bool, false}}
  defp keyword(_), do: :name
end


defmodule MiniLexer do
  def lex(text), do: scan(text, [])

  defp scan("", acc) do
    # EXERCISE AP2-E1a BEGIN
    {:ok, Enum.reverse(acc)}
    # EXERCISE AP2-E1a END
  end

  defp scan(" " <> rest, acc), do: scan(rest, acc)
  defp scan("\t" <> rest, acc), do: scan(rest, acc)
  defp scan("\r" <> rest, acc), do: scan(rest, acc)
  defp scan("\n" <> rest, acc), do: scan(rest, acc)
  defp scan("==" <> rest, acc), do: scan(rest, [:eq | acc])
  defp scan("->" <> rest, acc), do: scan(rest, [:arrow | acc])

  defp scan("+" <> rest, acc) do
    # EXERCISE AP2-E1b BEGIN
    scan(rest, [:plus | acc])
    # EXERCISE AP2-E1b END
  end

  defp scan("-" <> rest, acc), do: scan(rest, [:minus | acc])
  defp scan("*" <> rest, acc), do: scan(rest, [:multiply | acc])
  defp scan("/" <> rest, acc), do: scan(rest, [:divide | acc])
  defp scan("<" <> rest, acc), do: scan(rest, [:lt | acc])
  defp scan("=" <> rest, acc), do: scan(rest, [:assign | acc])
  defp scan("(" <> rest, acc), do: scan(rest, [:lparen | acc])
  defp scan(")" <> rest, acc), do: scan(rest, [:rparen | acc])
  defp scan("{" <> rest, acc), do: scan(rest, [:lbrace | acc])
  defp scan("}" <> rest, acc), do: scan(rest, [:rbrace | acc])
  defp scan("," <> rest, acc), do: scan(rest, [:comma | acc])

  defp scan(text, acc) do
    case MiniLiteral.read(text) do
      {:ok, token, rest} -> scan(rest, [token | acc])
      {:error, reason} -> {:error, {:lex, reason, text}}
    end
  end
end


defmodule MiniParser do
  def parse(tokens) do
    case sequence(tokens) do
      {:ok, tree, []} -> {:ok, tree}
      {:ok, _tree, rest} -> error(:unexpected_tokens, rest)
      error -> error
    end
  end

  defp sequence([:let, {:name, name}, :assign | rest]) do
    with {:ok, value, rest} <- expression(rest),
         {:ok, body, rest} <- sequence(rest) do
      {:ok, {:let, name, value, body}, rest}
    end
  end

  defp sequence(tokens), do: expression(tokens)
  defp expression(tokens), do: binary(tokens, &comparison/1, [:eq])
  defp comparison(tokens), do: binary(tokens, &sum/1, [:lt])
  defp sum(tokens), do: binary(tokens, &product/1, [:plus, :minus])
  defp product(tokens), do: binary(tokens, &unary/1, [:multiply, :divide])

  defp binary(tokens, read, operators) do
    with {:ok, left, rest} <- read.(tokens) do
      continue_binary(left, rest, read, operators)
    end
  end

  defp continue_binary(left, [op | rest] = tokens, read, operators) do
    if op in operators do
      with {:ok, right, rest} <- read.(rest) do
        # EXERCISE AP2-E3b BEGIN
        tree = {:binary, op, left, right}
        # EXERCISE AP2-E3b END
        continue_binary(tree, rest, read, operators)
      end
    else
      {:ok, left, tokens}
    end
  end

  defp continue_binary(left, [], _read, _operators), do: {:ok, left, []}

  defp unary([:minus | rest]) do
    with {:ok, tree, rest} <- unary(rest), do: {:ok, {:neg, tree}, rest}
  end

  defp unary(tokens) do
    with {:ok, tree, rest} <- primary(tokens), do: calls(tree, rest)
  end

  defp calls(tree, [:lparen | rest]) do
    with {:ok, arguments, rest} <- arguments(rest) do
      calls({:call, tree, arguments}, rest)
    end
  end

  defp calls(tree, rest), do: {:ok, tree, rest}
  defp arguments([:rparen | rest]), do: {:ok, [], rest}
  defp arguments(tokens), do: argument_list(tokens, [])

  defp argument_list(tokens, acc) do
    with {:ok, argument, rest} <- expression(tokens) do
      case rest do
        [:comma | rest] -> argument_list(rest, [argument | acc])
        [:rparen | rest] -> {:ok, Enum.reverse([argument | acc]), rest}
        _ -> error(:expected_argument_separator, rest)
      end
    end
  end

  defp primary([{:number, number} | rest]) do
    # EXERCISE AP2-E3a BEGIN
    {:ok, {:number, number}, rest}
    # EXERCISE AP2-E3a END
  end

  defp primary([{:bool, value} | rest]), do: {:ok, {:bool, value}, rest}
  defp primary([{:name, name} | rest]), do: {:ok, {:name, name}, rest}

  defp primary([:lparen | rest]) do
    with {:ok, tree, rest} <- expression(rest),
         {:ok, rest} <- expect(rest, :rparen) do
      {:ok, tree, rest}
    end
  end

  defp primary([:lbrace | rest]), do: block(rest)

  defp primary([:case | rest]) do
    with {:ok, condition, rest} <- expression(rest),
         {:ok, rest} <- expect(rest, :lbrace),
         {:ok, rest} <- expect(rest, {:bool, true}),
         {:ok, rest} <- expect(rest, :arrow),
         {:ok, yes, rest} <- expression(rest),
         {:ok, rest} <- expect(rest, {:bool, false}),
         {:ok, rest} <- expect(rest, :arrow),
         {:ok, no, rest} <- expression(rest),
         {:ok, rest} <- expect(rest, :rbrace) do
      {:ok, {:case, condition, yes, no}, rest}
    end
  end

  defp primary([:fn, :lparen | rest]) do
    with {:ok, parameters, rest} <- parameters(rest),
         {:ok, rest} <- expect(rest, :lbrace),
         {:ok, body, rest} <- block(rest) do
      {:ok, {:fn, parameters, body}, rest}
    end
  end

  defp primary(tokens), do: error(:expected_expression, tokens)

  defp block(tokens) do
    with {:ok, tree, rest} <- sequence(tokens),
         {:ok, rest} <- expect(rest, :rbrace) do
      {:ok, tree, rest}
    end
  end

  defp parameters([:rparen | rest]), do: {:ok, [], rest}
  defp parameters(tokens), do: parameter_list(tokens, [])

  defp parameter_list([{:name, name} | rest] = tokens, acc) do
    if name in acc do
      error(:duplicate_parameter, tokens)
    else
      case rest do
        [:comma | rest] -> parameter_list(rest, [name | acc])
        [:rparen | rest] -> {:ok, Enum.reverse([name | acc]), rest}
        _ -> error(:expected_parameter_separator, rest)
      end
    end
  end

  defp parameter_list(tokens, _acc), do: error(:expected_parameter, tokens)
  defp expect([token | rest], token), do: {:ok, rest}
  defp expect(tokens, token), do: error({:expected, token}, tokens)
  defp error(reason, tokens), do: {:error, {:parse, reason, tokens}}
end


defmodule MiniEnv do
  def fetch([], name), do: {:error, {:undefined_name, name}}

  def fetch([{name, value} | _rest], name) do
    # EXERCISE AP2-E4a BEGIN
    {:ok, value}
    # EXERCISE AP2-E4a END
  end

  def fetch([_binding | rest], name), do: fetch(rest, name)
end
```

## E1 — Une fonction conserve son environnement (35 min)

Le parser construit `{:fn, parametres, corps}` pour une définition.
Évaluer ce nœud ne doit pas encore calculer le corps : il peut contenir
des paramètres qui n'ont pas de valeur à cet instant.

Nous fabriquons une *fermeture*, ou *closure* :
`{:closure, parametres, corps, environnement_capture}`. Le dernier élément
est l'environnement présent lors de la définition. Cette donnée contient
ce qui sera nécessaire pour exécuter le corps plus tard.

Dans `MiniEval`, complétez les emplacements **AP3-E1**. Laissez ceux de
l'appel pour E2. Observez d'abord une fermeture sans chercher à l'appeler.


```elixir
defmodule MiniEval do
  def eval(tree, env \\ [])

  def eval({:number, number}, _env) do
    # EXERCISE AP2-E2a BEGIN
    {:ok, number}
    # EXERCISE AP2-E2a END
  end

  def eval({:bool, value}, _env), do: {:ok, value}
  def eval({:name, name}, env), do: MiniEnv.fetch(env, name)
  def eval({:neg, tree}, env), do: negate(eval(tree, env))

  def eval({:binary, op, left, right}, env) do
    # EXERCISE AP2-E2b BEGIN
    combine(op, eval(left, env), eval(right, env))
    # EXERCISE AP2-E2b END
  end

  def eval({:let, name, value, body}, env) do
    bind(name, eval(value, env), body, env)
  end

  def eval({:case, condition, yes, no}, env) do
    choose(eval(condition, env), yes, no, env)
  end

  def eval({:fn, parameters, body}, env) do
    # EXERCISE AP3-E1 BEGIN
    {:error, :functions_not_implemented}
    # EXERCISE AP3-E1 END
  end

  def eval({:call, function, arguments}, env) do
    call(eval(function, env), arguments, env)
  end

  defp negate({:ok, number}) when is_float(number), do: {:ok, -number}
  defp negate({:ok, _}), do: {:error, :expected_number}
  defp negate({:error, _} = error), do: error

  defp combine(_op, {:error, _} = error, _right), do: error
  defp combine(_op, _left, {:error, _} = error), do: error

  defp combine(op, {:ok, left}, {:ok, right}) do
    try do
      operation(op, left, right)
    rescue
      ArithmeticError -> {:error, :arithmetic_overflow}
    end
  end

  defp operation(op, left, right) when is_float(left) and is_float(right) do
    numeric(op, left, right)
  end

  defp operation(_op, _left, _right), do: {:error, :type_mismatch}
  defp numeric(:plus, left, right), do: {:ok, left + right}
  defp numeric(:minus, left, right), do: {:ok, left - right}
  defp numeric(:multiply, left, right), do: {:ok, left * right}
  defp numeric(:divide, _left, right) when right == 0.0, do: {:error, :division_by_zero}
  defp numeric(:divide, left, right), do: {:ok, left / right}
  defp numeric(:lt, left, right), do: {:ok, left < right}
  defp numeric(:eq, left, right), do: {:ok, left == right}

  defp bind(name, {:ok, value}, body, env) do
    # EXERCISE AP2-E4b BEGIN
    eval(body, [{name, value} | env])
    # EXERCISE AP2-E4b END
  end

  defp bind(_name, {:error, _} = error, _body, _env), do: error

  defp choose({:ok, true}, yes, _no, env) do
    # EXERCISE AP2-E5a BEGIN
    eval(yes, env)
    # EXERCISE AP2-E5a END
  end

  defp choose({:ok, false}, _yes, no, env) do
    # EXERCISE AP2-E5b BEGIN
    eval(no, env)
    # EXERCISE AP2-E5b END
  end

  defp choose({:ok, _}, _yes, _no, _env), do: {:error, :expected_boolean}
  defp choose({:error, _} = error, _yes, _no, _env), do: error

  defp call({:ok, {:closure, parameters, body, captured}}, arguments, caller) do
    if length(parameters) == length(arguments) do
      # EXERCISE AP3-E2a BEGIN
      values = nil
      # EXERCISE AP3-E2a END
      apply_function(values, parameters, body, captured)
    else
      {:error, {:arity, length(parameters), length(arguments)}}
    end
  end

  defp call({:ok, _value}, _arguments, _caller), do: {:error, :not_a_function}
  defp call({:error, _} = error, _arguments, _caller), do: error

  defp eval_arguments([], _env, acc), do: {:ok, Enum.reverse(acc)}

  defp eval_arguments([tree | rest], env, acc) do
    with {:ok, value} <- eval(tree, env), do: eval_arguments(rest, env, [value | acc])
  end

  defp apply_function({:ok, values}, parameters, body, captured) do
    # EXERCISE AP3-E2b BEGIN
    nil
    # EXERCISE AP3-E2b END
  end

  defp apply_function({:error, _} = error, _parameters, _body, _captured), do: error
  defp bind_parameters([], [], env), do: env

  defp bind_parameters([name | names], [value | values], env) do
    bind_parameters(names, values, [{name, value} | env])
  end
end
```


```elixir
defmodule Mini do
  def run(source) do
    with {:ok, tokens} <- MiniLexer.lex(source),
         {:ok, tree} <- MiniParser.parse(tokens) do
      case MiniEval.eval(tree, []) do
        {:ok, value} -> {:ok, value}
        {:error, reason} -> {:error, {:eval, reason}}
      end
    end
  end
end
```


```elixir
corps = {:binary, :plus, {:name, "x"}, {:name, "offset"}}
environnement = [{"offset", 2.0}]
ControleMini.egal(MiniEval.eval({:fn, ["x"], corps}, environnement),
  {:ok, {:closure, ["x"], corps, environnement}})
# Le corps contient un nom inconnu : définir la fonction doit tout de même réussir.
ControleMini.egal(MiniEval.eval({:fn, [], {:name, "later"}}, []),
  {:ok, {:closure, [], {:name, "later"}, []}})
IO.puts("E1 : fermeture construite sans exécuter son corps")
```

## E2 — Appeler une fonction (40 min)

Un appel contient trois éléments : `{:call, fonction, arguments}`.
`fonction` est elle-même un arbre : nous devons l'évaluer pour obtenir
une fermeture. Les arguments sont aussi des arbres, évalués de gauche
à droite dans l'environnement où l'appel apparaît.

Nous pouvons alors lier chaque paramètre à l'argument correspondant dans
une extension de l'environnement capturé, puis évaluer le corps.
L'environnement de l'appelant reste intact. Les fonctions fournies
propagent les erreurs ; complétez les emplacements **AP3-E2**.

Avant de coder, expliquez les étapes de cet appel :

```text
let scale = 10.0
let f = fn(x) { x * scale }
let scale = 2.0
f(scale + 1.0)
```

L'argument doit valoir 3.0, puis le corps multiplie par le `scale` capturé,
qui vaut 10.0. L'exemple oblige à distinguer deux environnements.

En AP3-E2a, liez `values` au résultat du helper
`eval_arguments(arguments, caller, [])`. En AP3-E2b, utilisez
`bind_parameters(parameters, values, captured)` pour obtenir le nouvel
environnement, puis évaluez `body` dans celui-ci. Ces helpers sont fournis,
mais prenez le temps de suivre leurs appels récursifs sur un exemple.


```elixir
ControleMini.proche(Mini.run("fn(x) { x * 2.0 }(3.0)"), 6.0)
ControleMini.proche(Mini.run("fn() { 4.0 }()"), 4.0)
ControleMini.proche(Mini.run("fn(a, b) { a - b }(8.0, 3.0)"), 5.0)
ControleMini.proche(Mini.run("let scale = 10.0 let f = fn(x) { x * scale } let scale = 2.0 f(scale + 1.0)"), 30.0)
ControleMini.egal(Mini.run("fn(x) { x }()"), {:error, {:eval, {:arity, 1, 0}}})
ControleMini.egal(Mini.run("fn() { 1.0 }(2.0)"), {:error, {:eval, {:arity, 0, 1}}})
ControleMini.egal(Mini.run("1.0(2.0)"), {:error, {:eval, :not_a_function}})
ControleMini.egal(Mini.run("fn(x) { x }(missing)"),
  {:error, {:eval, {:undefined_name, "missing"}}})
IO.puts("E2 : appels, arguments et erreurs vérifiés")
```

## E3 — Observer la portée lexicale (35 min)

Le résultat d'un appel dépend de l'environnement capturé lors de la
définition. C'est la *portée lexicale*. Nous allons vérifier ce choix
sur des programmes courts, en notant les prédictions avant l'exécution.

```text
let x = 2.0
let f = fn(y) { x + y }
let x = 100.0
f(3.0)
```

```text
let x = 7.0
let f = fn(x) { x + 1.0 }
f(2.0) + x
```

Pour chaque programme, indiquez l'environnement capturé, les liaisons
ajoutées lors de l'appel et le résultat final. Comparez avec ce qui se
passerait si nous utilisions l'environnement appelant dans le corps.

Ajoutez un troisième programme qui distingue les deux interprétations.
Écrivez sa vérification dans une nouvelle cellule de code.


```elixir
ControleMini.proche(Mini.run("let x = 2.0 let f = fn(y) { x + y } let x = 100.0 f(3.0)"), 5.0)
ControleMini.proche(Mini.run("let x = 7.0 let f = fn(x) { x + 1.0 } f(2.0) + x"), 10.0)
ControleMini.egal(Mini.run("let f = fn() { later } let later = 2.0 f()"),
  {:error, {:eval, {:undefined_name, "later"}}})
ControleMini.egal(Mini.run("let f = fn(x) { f(x) } f(1.0)"),
  {:error, {:eval, {:undefined_name, "f"}}})
IO.puts("E3 : portée lexicale et limites de la capture vérifiées")
```

## E4 — Passer et renvoyer des fonctions (30 min)

Une fermeture est une valeur. Elle peut être liée à un nom, passée comme
argument ou renvoyée par une autre fonction. Le mécanisme d'appel que vous
venez d'écrire doit donc suffire pour les exemples suivants.

```text
let apply_twice = fn(f, x) { f(f(x)) }
let add_one = fn(x) { x + 1.0 }
apply_twice(add_one, 3.0)
```

```text
let make_adder = fn(offset) { fn(x) { x + offset } }
let add_two = make_adder(2.0)
add_two(5.0)
```

Prédisez les résultats. Dans le deuxième exemple, que contient
l'environnement de `add_two` après la fin de l'appel à `make_adder` ?
Pourquoi la liaison `offset` reste-t-elle accessible à cette fermeture ?

Inventez ensuite une fonction qui fabrique un multiplicateur, puis testez
deux multiplicateurs obtenus à partir de facteurs différents.


```elixir
ControleMini.proche(Mini.run("let apply_twice = fn(f, x) { f(f(x)) } let add_one = fn(x) { x + 1.0 } apply_twice(add_one, 3.0)"), 5.0)
ControleMini.proche(Mini.run("let make_adder = fn(offset) { fn(x) { x + offset } } let add_two = make_adder(2.0) add_two(5.0)"), 7.0)
ControleMini.proche(Mini.run("let make = fn(n) { fn(x) { x + n } } make(2.0)(3.0)"), 5.0)
IO.puts("E4 : ordre supérieur et fermetures retournées vérifiés")
```

## Bilan de votre exploration (25 min)

Complétez cette cellule de texte et ajoutez vos vérifications :

1. Tracez un appel, en distinguant environnement appelant et environnement
   capturé. Indiquez dans lequel sont évalués les arguments et le corps.
2. Expliquez pourquoi évaluer une définition de fonction n'évalue pas
   immédiatement son corps.
3. Présentez votre programme personnel de portée lexicale et vos deux
   multiplicateurs, avec les résultats attendus puis obtenus.
4. Ajoutez deux appels incorrects : mauvais nombre d'arguments et valeur
   non appelable. Vérifiez qu'ils renvoient une erreur explicite.
5. Décrivez une erreur ou une hésitation et sa correction.

Pour documenter cette exploration facultative, conservez les exercices tentés,
vos explications et vos cas personnels. Les
extensions suivantes permettent d'aller plus loin sans modifier la note
du socle si vous ne les faites pas.

## Autres missions pour les rapides

Choisissez une mission et écrivez les cas attendus avant de modifier le
langage. Elles représentent environ une heure de travail supplémentaire
si vous en combinez plusieurs. Sauvegardez vos changements et relancez les
vérifications du socle après chaque étape.

### Mission A — Ajouter `!=` de bout en bout (20 à 30 min)

Le nouvel opérateur compare deux nombres,
à la même priorité que `==`. Modifiez le lexer pour reconnaître le token
`:neq`, le lecteur `expression/1` du parser pour l'accepter, puis les
helpers `operation` et `numeric` de l'évaluateur pour calculer le résultat.
Un mélange de types doit rester une erreur. Le nœud binaire existant suffit.

```text
Mini.run("2.0 != 3.0")
# {:ok, true}
Mini.run("True != True")
# {:error, {:eval, :type_mismatch}}
Mini.run("1.0 + 1.0 != 2.0")
# {:ok, false}
Mini.run("1.0 != True")
# {:error, {:eval, :type_mismatch}}
```

### Mission B — Un `&&` qui court-circuite (30 à 45 min)

Ajoutez `&&`, moins prioritaire que `==`. Les deux opérandes doivent être
booléens lorsqu'ils sont évalués. Si le premier vaut `False`, le second
n'est pas évalué. Un nœud d'arbre spécifique permet de distinguer ce
comportement des opérations binaires qui évaluent leurs deux enfants.

Modifiez donc lexer, grammaire et évaluateur. Ne réutilisez pas directement
`combine`, car ses deux arguments sont déjà évalués quand il est appelé.

```text
Mini.run("True && 1.0 < 2.0")
# {:ok, true}
Mini.run("False && 1.0 / 0.0 == 0.0")
# {:ok, false}
Mini.run("True && 1.0 / 0.0 == 0.0")
# {:error, {:eval, :division_by_zero}}
Mini.run("1.0 && True")
# {:error, {:eval, :expected_boolean}}
Mini.run("True && 1.0")
# {:error, {:eval, :expected_boolean}}
```

### Mission C — Compter les nœuds visités (30 min)

Ajoutez une fonction d'évaluation instrumentée qui renvoie aussi le nombre
de nœuds réellement visités. Fixez la règle suivante : une feuille, un
nœud binaire ou un `case` visité compte chacun pour 1 ; les nœuds de la
branche ignorée ne comptent pas. Commencez par les arbres manuels,
puis raccordez le parser. Gardez l'évaluateur d'origine pour comparer
les valeurs obtenues.

```text
1.0 + 2.0 * 3.0
# résultat 7.0 ; 5 nœuds visités
case True { True -> 42.0 False -> 1.0 / 0.0 }
# résultat 42.0 ; 3 nœuds visités
```

Le compteur doit suivre l'évaluation effective : compter d'abord tous les
nœuds de l'arbre donnerait un mauvais résultat pour le deuxième cas.

## Pour poursuivre — Choisissez une extension

Avant de modifier le langage, écrivez un programme qui doit réussir et un
programme qui doit échouer. Précisez le résultat attendu, puis identifiez
les étapes à changer : lexer, parser, arbre, évaluateur ou environnement.

- Ajoutez une comparaison, par ex. `<=`. Vérifiez aussi le préfixe `<`
  pour ne pas casser les programmes existants.
- Ajoutez un opérateur booléen qui court-circuite son deuxième argument.
  Un cas avec une erreur dans l'argument ignoré doit rester valide.
- Ajoutez une fonction native numérique, par ex. une valeur absolue.
  Précisez son nombre d'arguments et les types acceptés.
- Produisez une trace des nœuds visités pendant l'évaluation, afin de voir
  quelle branche d'un `case` est réellement parcourue.
- Étudiez la récursion nommée. Avec le mécanisme actuel,
  `let f = fn(x) { f(x) } f(1.0)` échoue : `f` n'existait pas lors de la
  capture. Il faut choisir une règle pour la définition récursive et lui
  donner une représentation explicite ; ajouter seulement un test ne suffit pas.

Chaque extension doit conserver les résultats du socle. Nous ne demandons
pas d'ajouter une puissance à ce laboratoire.

## Sauvegarder votre entraînement

Enregistrez ce fichier sous `entrainement/ap3-fonctions.livemd` dans votre
dépôt privé `ap-homework`. Je préciserai en cours les étapes de compilation
`ap3-01` à `ap3-04` à terminer pour le rendu unique du cycle. Ce laboratoire
reste facultatif ; vous pouvez le joindre en complément pour un bonus.
Relancez les cellules dans l'ordre et conservez vos tentatives expliquées.

Pour toute question, vous pouvez me contacter par email :
[aurelien.noce@minesparis.psl.eu](mailto:aurelien.noce@minesparis.psl.eu).
