# AP3 · 5 — Appeler une fonction (facultatif)

## Notre question

Notre compilateur refuse `fn` et les appels. Que manque-t-il à la machine pour
exécuter `let inc = fn(x) { x + 1.0 } inc(4.0)` ? Quand `inc` termine, la
machine doit **revenir** juste après l'appel et retrouver les variables de
l'appelant. Nous ajoutons trois instructions et une pile de *cadres d'appel*.
Les fonctions restent **fermées** : leur corps ne voit que ses paramètres.

> **Repères.** Prolongement **facultatif et hors rendu**, pour qui a terminé les quatre étapes, ou pour toute la classe en « locomotive » après la mise en commun de l'étape 4. [Slides 49 à 52](https://ap-2026.noce.fr/slides/AP3.html#49) : cadres d'appel, fonctions fermées. Elixir : `Enum.split/2`, `Enum.reverse/1`, une liste de cadres.

Vous partez d'arbres d'AP2 écrits à la main : aucun téléchargement, aucun
parser terminé à importer. À partir de l'étape 2, copiez vos propres modules
précédents aux emplacements indiqués, car chaque Livebook a sa propre session.
Réévaluez la cellule entière après avoir modifié un module ; les vérifications
signalent une erreur tant que les fonctions demandées ne sont pas écrites.
Prédisez d'abord leurs résultats. Vous pouvez ajouter des fonctions auxiliaires.

> 💡 Ce prolongement est facultatif et hors rendu.

La cellule suivante définit `Check`, notre petit outil de vérification :
`Check.equal(obtenu, attendu)` lève une erreur, avec les deux valeurs, si elles
diffèrent ; `Check.error(valeur)` exige un tuple `{:error, raison}`.

```elixir
defmodule Check do
  def equal(actual, expected) do
    if actual != expected do
      raise "Attendu #{inspect(expected)}, obtenu #{inspect(actual)}"
    end
    :ok
  end
  def error({:error, _}), do: :ok
  def error(value), do: raise("Une erreur était attendue : #{inspect(value)}")
end
```

## Trois instructions et une pile de cadres

| Instruction | Opcode | Taille | Effet |
| --- | ---: | ---: | --- |
| `{:push_fn, adresse, arité}` | 15 | 4 | Empiler la valeur fonction `{:fn, adresse, arité}`. |
| `{:call, n}` | 16 | 2 | Dépiler `n` arguments puis la fonction ; empiler un cadre ; sauter. |
| `:return` | 17 | 1 | Dépiler le cadre ; restaurer les variables ; revenir après l'appel. |

Lisons ce tableau. L'*arité* est le nombre de paramètres d'une fonction ; elle
tient sur un octet, et l'adresse, comme un saut, sur deux. Un *cadre* conserve
l'adresse de retour, c-à-d. celle de l'instruction qui suit `call`, et la
mémoire locale de l'appelant. L'appelé repart avec une mémoire vide : ses
arguments sont encore sur la pile, et son *prologue*, les premières
instructions de son code, les range dans ses cases par `store`, **dans l'ordre
inverse** de leur empilement, puisque le dernier argument empilé est au sommet.
Au `return`, la valeur calculée reste au sommet ; la mémoire de l'appelant est
restaurée.

Où placer le code d'une fonction ? Sur place, sauté par un `jump` : créer la
valeur fonction n'exécute pas son corps, et cette valeur n'est qu'une adresse.
Pour `fn(x) { x }(4.0)` :

```text
jump after_0        ; 0  : ne pas exécuter le corps à la définition
fn_0:
store 0             ; 3  : le prologue range l'argument dans la case de x
load 0              ; 6
return              ; 9
after_0:
push_fn fn_0 1      ; 10 : la valeur fonction, adresse 3 et arité 1
push 4.0            ; 14
call 1              ; 23 : empile le cadre {25, mémoire}, saute en 3
halt                ; 25
```

Lisons ce listing, dont les points-virgules annotent seulement l'adresse de
chaque ligne. La machine commence par sauter le corps, empile la valeur
fonction puis l'argument, et `call 1` ouvre un cadre dont l'adresse de retour
est `25` avant de sauter en `3`. Le corps range `4.0` dans la case `0`, la
relit et `return` revient en `25` avec `4.0` au sommet. Prédisez la pile, la
mémoire et la profondeur des cadres avant chaque instruction visitée, puis le
résultat. Que se passe-t-il si l'on appelle avec deux arguments ?

## Reprendre vos modules

Copiez **votre `MiniBytecode` de l'étape 4** et ajoutez : les trois tailles,
l'encodage et le décodage des trois instructions, et la résolution de
l'étiquette d'un `push_fn` dans la seconde passe de l'assembleur. L'arité tient
sur un octet ; l'adresse, comme un saut, sur deux.

```elixir
# Copiez ici votre MiniBytecode de l'étape 4.
# Ajoutez size/1, encode/1 et decode pour push_fn (15, 4 octets), call (16, 2 octets)
# et return (17, 1 octet) ; résolvez l'étiquette de {:push_fn, label, arity} en passe 2.
```

> 💡 `Enum.reduce(parameters, {%{}, state}, fn name, {local, state} -> … end)`
> parcourt les paramètres en construisant à la fois un environnement et un
> état ; `length(parameters)` compte les paramètres, et `Enum.reverse/1`
> inverse une liste.

Copiez **votre `MiniCompiler` de l'étape 4** et remplacez les deux clauses
« hors cible » par de vraies clauses. Pour `{:fn, parameters, body}` : réservez
deux étiquettes, attribuez une case à chaque paramètre, compilez le corps avec
**un environnement réduit aux paramètres**, puis assemblez `jump`, étiquette,
prologue, corps, `return`, étiquette, `push_fn`. Pour `{:call, function, arguments}` :
compilez la fonction, puis chaque argument dans l'ordre, puis `{:call, n}`.

```elixir
# Copiez ici votre MiniCompiler de l'étape 4.
# Remplacez les clauses {:fn, _, _} et {:call, _, _} : code sur place sauté par un jump,
# prologue de store dans l'ordre inverse, environnement du corps réduit aux paramètres.
```

> 💡 `Enum.split([1, 2, 3], 2)` rend `{[1, 2], [3]}` : les deux premiers
> éléments, puis le reste. `length(frames)` donne la profondeur de la pile
> des cadres, une liste dont la tête est le cadre le plus récent.

Copiez **votre `MiniVM` de l'étape 4** et ajoutez un paramètre `frames` à
`loop` et `step`. `push_fn` empile `{:fn, adresse, arité}` ; `call` vérifie que
la valeur sous les arguments est une fonction de la bonne arité, empile le cadre
`{adresse_suivante, mémoire}` et repart avec une mémoire vide ; `return` dépile
le cadre et restaure la mémoire. La validation des cibles vaut aussi pour
`push_fn`. Ajoutez la profondeur des cadres à la trace, sous la clé `depth`.

```elixir
# Copiez ici votre MiniVM de l'étape 4.
# Ajoutez frames à loop/7 et step/5, puis les transitions push_fn, call et return.
# Validez l'adresse d'un push_fn comme celle d'un saut ; tracez la profondeur des cadres.
```

## Vérifier les appels

Lisez chaque cas avant de l'exécuter. Le premier bloc vérifie le format des
trois instructions ; le deuxième retrouve le listing de l'identité, ses
adresses visitées et la profondeur des cadres, `1` pendant le corps ; les
suivants appellent `twice`, une soustraction à deux arguments et une fonction
sans paramètre. Les derniers sont des refus : une variable libre à la
compilation, une arité fausse, un appel d'un nombre et un `return` sans
cadre. Si vous avez votre interpréteur d'AP2 sous la main, comparez-le à la
machine sur ces programmes fermés.

```elixir
Check.equal(MiniBytecode.encode({:push_fn, 258, 2}), {:ok, <<15, 1, 2, 2>>})
Check.equal(MiniBytecode.encode({:call, 1}), {:ok, <<16, 1>>})
Check.equal(MiniBytecode.decode_at(<<15, 1, 2, 2, 16, 1, 17>>, 4), {:ok, {:call, 1}, 6})
Check.equal(MiniBytecode.assemble([{:push_fn, :missing, 0}]), {:error, {:unknown_label, :missing}})

identity = {:call, {:fn, ["x"], {:name, "x"}}, [{:number, 4.0}]}
{:ok, program} = MiniCompiler.compile(identity)
Check.equal(MiniBytecode.disassemble(program), {:ok,
  [{0, {:jump, 10}}, {3, {:store, 0}}, {6, {:load, 0}}, {9, :return},
   {10, {:push_fn, 3, 1}}, {14, {:push, 4.0}}, {23, {:call, 1}}, {25, :halt}]})
{:ok, 4.0, trace} = MiniVM.trace(program)
Check.equal(Enum.map(trace, & &1.pc), [0, 10, 14, 23, 3, 6, 9, 25])
Check.equal(Enum.map(trace, & &1.depth), [0, 0, 0, 0, 1, 1, 1, 0])

inc = {:fn, ["x"], {:binary, :plus, {:name, "x"}, {:number, 1.0}}}
twice = {:fn, ["f", "x"], {:call, {:name, "f"}, [{:call, {:name, "f"}, [{:name, "x"}]}]}}
program_twice = {:let, "twice", twice, {:let, "inc", inc,
  {:call, {:name, "twice"}, [{:name, "inc"}, {:number, 1.0}]}}}
{:ok, program} = MiniCompiler.compile(program_twice)
{:ok, 3.0, trace} = MiniVM.trace(program)
Check.equal(Enum.max(Enum.map(trace, & &1.depth)), 2)
Check.equal(map_size(List.last(trace).locals), 2)

subtract = {:call, {:fn, ["a", "b"], {:binary, :minus, {:name, "a"}, {:name, "b"}}},
  [{:number, 8.0}, {:number, 3.0}]}
{:ok, program} = MiniCompiler.compile(subtract)
Check.equal(MiniVM.run(program), {:ok, 5.0})
{:ok, program} = MiniCompiler.compile({:call, {:fn, [], {:number, 7.0}}, []})
Check.equal(MiniVM.run(program), {:ok, 7.0})

free = {:let, "offset", {:number, 2.0},
  {:call, {:fn, ["x"], {:binary, :plus, {:name, "x"}, {:name, "offset"}}}, [{:number, 1.0}]}}
Check.equal(MiniCompiler.compile(free), {:error, {:undefined_name, "offset"}})
{:ok, program} = MiniCompiler.compile({:call, {:fn, ["x"], {:name, "x"}}, []})
Check.error(MiniVM.run(program))
{:ok, program} = MiniCompiler.compile({:call, {:number, 1.0}, [{:number, 2.0}]})
Check.error(MiniVM.run(program))
Check.equal(MiniVM.run(<<1, 7.0::float-big-64, 17>>), {:error, {9, :return_outside_call}})
```

## Indices et prolongements

**Premier indice.** Dans `call`, `Enum.split(stack, n)` sépare les `n`
arguments du reste de la pile ; la valeur fonction est alors en tête du reste.
Les arguments repartent sur la pile de l'appelé, dans le même ordre.

**Deuxième indice.** Le prologue range les arguments par `store` dans
l'ordre inverse des paramètres : le dernier argument empilé est au sommet.

**Troisième indice.** L'environnement du corps est une map neuve contenant
les seuls paramètres ; le compteur de cases, lui, continue d'avancer. Deux
appels imbriqués n'ont donc jamais la même mémoire : chaque `call` repart de `%{}`.

### Pour aller plus loin

Pourquoi `let k = fn(x) { fn(y) { x } }` est-il refusé ? Une *fermeture*
demanderait d'embarquer, dans la valeur fonction, les variables capturées à
la définition : c'est ce que fait `{:closure, …}` dans l'interpréteur d'AP2.
Concevez ce format, puis un `let` récursif : que doit contenir la mémoire de
l'appelé pour que la fonction puisse s'appeler elle-même ?

## Garder une trace de votre approfondissement

Conservez votre code, un résultat prédit puis observé et une trace des cadres
au moment d'un appel. Expliquez ce qui est propre à chaque appel et ce que
la machine restaure au retour. Le notebook est facultatif et hors rendu.
Gardez-le dans `entrainement/` ; il peut aussi accompagner une reprise collective.
Quand un bout de syntaxe vous manque, le [poly](https://ap-2026.noce.fr/poly/poly-elixir.pdf) en donne la forme avec
un exemple. Pour toute question :
[aurelien.noce@minesparis.psl.eu](mailto:aurelien.noce@minesparis.psl.eu).
