# AP3 · 3 — Où vont les variables ?

## Notre question

Une VM peut calculer sans AST. Nous allons maintenant faire disparaître les
noms de variables du programme compilé, tout en conservant leur *portée
lexicale*, c-à-d. la règle qui dit, pour chaque `x` du texte, quelle
déclaration il désigne.

> **Repères.** [Slides 31 à 38](https://ap-2026.noce.fr/slides/AP3.html#31) : cases locales, table des noms, portée. Elixir : maps `%{}`, `Map.fetch/2`, `Map.put/3`. Poly, chap. 3 et 6.

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.

> 💡 Les étapes suivent une progression ; je préciserai en cours les notebooks à travailler et ceux à rendre.

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}`. Évaluez-la
une fois, puis lancez les cellules de vérification après chaque modification.

```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
```

## Deux tables, deux moments

> 💡 Une *map* associe des clés à des valeurs. `Map.put(env, "x", 2)` renvoie
> une nouvelle map où `"x"` vaut `2` ; l'ancienne reste disponible, gardez-la
> pour revenir à une portée extérieure. `Map.fetch(env, "x")` rend
> `{:ok, 2}`, ou `:error` si la clé manque. Dans `%{slot: 0}`, `slot:` est la
> notation courte pour la clé atome `:slot`, et `state.slot` lit cette clé.
> `%{state | slot: 1}` remplace une clé existante dans une nouvelle map, sans
> modifier `state`.

À la compilation, l'environnement associe un **nom à une case** :
`%{"x" => 0}`. À l'exécution, la mémoire de la VM associe une **case à une
valeur** : `%{0 => 2.0}`. Ces deux maps ne contiennent donc pas la même
information et n'existent pas au même moment : les numéros de cases se
décident avant de connaître les valeurs, et le binaire ne transporte que des
numéros.

Deux instructions font le lien. `store 0` dépile une valeur et l'écrit dans la
case `0`. `load 0` lit cette case et empile sa valeur, sans la supprimer de la
mémoire. Dans les deux cas, le PC avance de trois octets. Nous n'avons ni
adresses mémoire réelles, ni pointeurs, ni registres de calcul nommés : les
cases sont une petite mémoire locale abstraite, une map indexée par des
entiers.

### Découvrir le souci du masquage

Prédisez le résultat de ce programme, puis dessinez ses deux environnements,
celui du bloc et celui de l'extérieur :

```text
let x = 2.0
let y = { let x = 7.0 x }
x + y
```

Le résultat attendu est `9.0`. Si les deux déclarations de `x` utilisaient la
même case, que perdrait-on ? Et si l'on réutilisait le compteur de cases de
l'environnement extérieur en sortant du bloc, la prochaine déclaration
pourrait-elle écraser une valeur encore utile ?

Nous choisissons une règle simple : **chaque déclaration reçoit une case
neuve**. Le compteur augmente partout pendant la compilation, y compris en
sortant d'un bloc. En revanche, un nom déclaré dans un sous-arbre ne devient
pas visible dans les autres sous-arbres : la table des noms, elle, suit la
portée.

## Reprise — VM et instructions de mémoire

Copiez **vos modules `MiniBytecode` et `MiniVM` de l'étape 2** dans cette
cellule. Le codec sait déjà encoder `load` et `store`. Ajoutez leurs
transitions dans `MiniVM.step/4`, avant votre clause générale d'erreur :
`store` dépile la valeur et l'écrit avec `Map.put(locals, slot, value)` ;
`load` cherche la case avec `Map.fetch/2` et refuse une case absente avec
`{:error, {:uninitialized_slot, numéro}}` ; `store` refuse une pile vide.
Décrivez ce qui change dans le triplet « PC, pile, cases » pour chacune.

Avant les ajouts, vérifiez que `8.0 - 3.0` fonctionne encore. Aucun saut n'est
encore nécessaire dans cette version.

```elixir
# Copiez ici votre MiniBytecode et votre MiniVM de l'étape 2.
# Ajoutez les transitions load et store dans MiniVM.step/4.
```

### Vérifier la continuité

Avant de compiler des variables, retrouvons le programme arithmétique précédent.

```elixir
{:ok, previous} = MiniBytecode.assemble([{:push, 8.0}, {:push, 3.0}, :minus, :halt])
Check.equal(MiniVM.run(previous), {:ok, 5.0})
```

## Concevoir la résolution des noms et des déclarations

Reprenez vos clauses de `MiniCompiler.emit/1` dans ce nouveau module, où elles
deviennent des clauses de `emit/3`. Le compilateur transporte désormais `env`,
la table des noms de la portée courante, et `state`, une map dont la clé
`slot` est la prochaine case libre. Deux nœuds d'AP2 arrivent : `{:name, "x"}`
lit une variable, `{:let, "x", valeur, suite}` la déclare pour la suite.
Écrivez entièrement les clauses de `emit/3` pour `name` et `let`. Le contrat
est `{:ok, instructions, nouvel_état}` ou une erreur.

- Pour `{:name, name}`, cherchez la case dans l'environnement et émettez un
  `load`, ou rendez `{:error, {:undefined_name, name}}`.
- Pour `{:let, name, valeur, suite}`, la valeur se compile dans l'environnement
  existant. Elle peut elle-même contenir des déclarations et consommer des
  cases. Puis une nouvelle case reçoit son résultat par un `store` ; seule la
  suite voit le nouveau nom.
- Adaptez vos autres clauses : chaque appel récursif reçoit l'environnement et
  le dernier état obtenu ; chaque réponse rend aussi cet état. Le compilateur
  ne rend pas son environnement local au parent ; il rend seulement le compteur.

`let` ne définit pas ici de liaison récursive. `let x = x x` doit donc refuser
un `x` initialement absent, même si la déclaration porte le même nom.

```elixir
defmodule MiniCompiler do
  def compile(tree) do
    with {:ok, code, _state} <- emit(tree, %{}, %{slot: 0}) do
      MiniBytecode.assemble(code ++ [:halt])
    end
  end

  defp emit({:number, value}, _env, state), do: {:ok, [{:push, value}], state}
  # Adaptez ici vos clauses bool, neg et binary de l'étape 2 à emit/3.
  # Ajoutez les clauses name et let.
  defp emit(_tree, _env, _state), do: {:error, :a_concevoir}
end
```

## Voir la portée dans le bytecode

> 💡 `for {_pc, {:store, slot}} <- listing, do: slot` parcourt le listing et
> ne garde que les éléments qui correspondent au motif, ici les `store`, pour
> rendre la liste de leurs cases. `List.last(trace).locals` lit la mémoire du
> dernier état.

Construisons l'arbre du programme précédent. Prédisez la case de chaque
déclaration avant d'afficher le listing : le `x` extérieur, le `x` du bloc,
puis `y`. Le résultat attendu est `9.0`, et la mémoire finale conserve les
trois cases, y compris celle du `x` intérieur que plus aucun nom ne désigne.
Les cas suivants sont des refus à la compilation : un nom jamais déclaré, un
`let x = x x`, un `x` du bloc lu depuis l'extérieur. Vient ensuite un binaire
écrit à la main qui lit la case `12` sans l'avoir remplie, puis un masquage
`let x = 2.0 let x = x + 1.0 x` où la valeur du second `x` se calcule avec le
premier.

```elixir
tree = {:let, "x", {:number, 2.0},
  {:let, "y", {:let, "x", {:number, 7.0}, {:name, "x"}},
    {:binary, :plus, {:name, "x"}, {:name, "y"}}}}
{:ok, binary} = MiniCompiler.compile(tree)
{:ok, listing} = MiniBytecode.disassemble(binary)
Check.equal(for({_pc, {:store, slot}} <- listing, do: slot), [0, 1, 2])
{:ok, value, trace} = MiniVM.trace(binary)
Check.equal(value, 9.0)
Check.equal(List.last(trace).locals, %{0 => 2.0, 1 => 7.0, 2 => 7.0})
Check.equal(List.last(trace).stack, [9.0])
Check.equal(MiniCompiler.compile({:name, "absent"}), {:error, {:undefined_name, "absent"}})
Check.error(MiniCompiler.compile({:let, "x", {:name, "x"}, {:name, "x"}}))
Check.error(MiniCompiler.compile({:let, "y", {:let, "x", {:number, 1.0}, {:name, "x"}}, {:name, "x"}}))
{:ok, bad} = MiniBytecode.assemble([{:load, 12}, :halt])
Check.equal(MiniVM.run(bad), {:error, {0, {:uninitialized_slot, 12}}})
{:ok, shadow} = MiniCompiler.compile({:let, "x", {:number, 2.0},
  {:let, "x", {:binary, :plus, {:name, "x"}, {:number, 1.0}}, {:name, "x"}}})
Check.equal(MiniVM.run(shadow), {:ok, 3.0})
```

## Indices et mise en commun

**Premier indice.** Un même nom écrit dans deux portées peut désigner deux cases
différentes. L'identité d'une déclaration ne se réduit pas à son orthographe.

**Deuxième indice.** Prenez le numéro de case dans l'état rendu par la compilation
de la valeur, puis incrémentez-le avant de compiler la suite.

**Troisième indice.** La séquence émise est « calculer la valeur ; la stocker ;
calculer la suite ». `Map.put(env, name, slot)` produit l'environnement de la
suite sans modifier celui du parent.

Expliquez où se trouve encore le nom `x` à chacun de ces moments : dans l'AST,
dans le binaire, dans la mémoire de la VM. Si vous avancez vite, proposez une
stratégie de réutilisation des cases ; donnez un contre-exemple à une stratégie
qui réutiliserait trop tôt la case d'une variable encore accessible.

## Avant de reprendre les slides

Conservez votre tentative, un résultat prédit puis observé et une explication
avec vos mots. Une fonction inachevée accompagnée d'une trace juste permet déjà
de discuter de l'idée ; la mise en commun sert à comparer vos choix et à
débloquer le point nécessaire pour poursuivre. À l'étape suivante, vous copierez
votre propre code dans les cellules indiquées ; aucun module n'est importé
implicitement d'un autre notebook.

Sauvegardez ce fichier dans `AP3/`. Je préciserai en cours les notebooks
à terminer et le minimum attendu, en fonction de notre avancement. Les étapes
suivantes restent disponibles pour approfondir ; les prolongements non
demandés sont facultatifs et peuvent apporter un bonus.
Ce travail rejoint le rendu unique du cycle AP1–AP3 le 6 décembre 2026 à
23 h 59 (Paris), sans devoir intermédiaire. AP4 s'ouvrira par un quiz de dix
minutes sur cette séance : relisez le chapitre 6 du poly, « Des octets aux
instructions ». Nous discuterons aussi de vos choix à l'oral. 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 : le
langage aux chapitres 1 à 4, les octets et les binaires au chapitre 6. Pour
toute question :
[aurelien.noce@minesparis.psl.eu](mailto:aurelien.noce@minesparis.psl.eu).
