# AP2 · 1 — Calculer sans garder d'arbre

## Notre question

Nous allons fabriquer une première calculatrice pour notre langage. Pour
commencer, nous nous donnons une règle volontairement simple : chaque
opération s'applique immédiatement au résultat déjà obtenu, en lisant de
gauche à droite, sans tenir compte des priorités habituelles. Prédisez le
résultat de ces trois programmes **avec cette règle**, avant de coder :

```text
8.0 - 3.0 - 2.0
1.0 + 2.0 * 3.0
(1.0 + 2.0) * 3.0
```

Notre premier langage ne connaît que les décimaux positifs ou nuls, écrits
avec au moins un chiffre de chaque côté du point (`0.0`, `12.5`), les quatre
opérations et les espaces. Les parenthèses sont reconnues par le lexer mais
refusées par la calculatrice ; le moins unaire viendra à l'étape 2. Un point
compte dès maintenant : une entrée incorrecte doit être signalée, et non
acceptée silencieusement sur son seul début.

> **Repères.** [Slides 7 à 13](https://ap-2026.noce.fr/slides/AP2.html#7) : tokens et calcul direct. Elixir : `defp`, gardes `when`, `with`, motifs sur les chaînes, repris au point d'emploi. Poly, chap. 4 « Fonctions, clauses, récursion » et chap. 5 « Construire un langage », § 5.1 « Du texte aux tokens » et § 5.2.1 « Calculer au fil de la lecture ».

> 💡 Point de départ conseillé : écrivez et testez cette étape en séance.
> Je préciserai en cours les notebooks à travailler et ceux à rendre.
> **Sauvegardez-le dès maintenant sous `AP2/ap2-01-calcul-direct.livemd`.**

## Reconnaître les morceaux du texte

Le *lexer* est la brique qui découpe le texte en *tokens*, les « mots » du
langage : un nombre, un opérateur, une parenthèse. C'est le lecteur de tokens
du carnet 4 d'AP1, appliqué à un alphabet un peu plus riche. La lecture d'un
nombre vous est fournie ci-dessous ; vous pouvez l'utiliser sans détailler la
regex. Elle impose des chiffres de part et d'autre du point et refuse qu'une
lettre, un chiffre ou un point suive immédiatement le nombre, ce qui rejette
`1.0e2` comme `1.2.3`. Son contrat est `{:ok, nombre, reste}` en cas de
succès, `{:error, raison}` sinon : le reste de la chaîne est rendu pour que
le parcours continue.

> 💡 `try … rescue` rattrape l'exception que `String.to_float/1` lève pour un
> nombre trop grand pour un flottant, et la transforme en erreur ordinaire.
> Vous n'avez pas à écrire cette forme aujourd'hui ; elle est là pour que
> toute lecture rende un résultat étiqueté.

```elixir
defmodule DecimalLiteral do
  def read(source) do
    case Regex.run(~r/^[0-9]+\.[0-9]+(?![a-zA-Z0-9_.])/, source) do
      [literal] ->
        try do
          {:ok, String.to_float(literal), String.replace_prefix(source, literal, "")}
        rescue
          ArgumentError -> {:error, :number_out_of_range}
        end
      nil -> {:error, {:invalid_token, source}}
    end
  end
end
```

Le lexer ci-dessous parcourt le texte avec un accumulateur, comme vos lecteurs
d'AP1. Il montre deux cas : un blanc à ignorer et le symbole `+`. Ajoutez par
analogie les clauses pour `-`, `*`, `/`, `(` et `)`, avec les tokens `:minus`,
`:multiply`, `:divide`, `:lparen` et `:rparen`. Placez-les avant la dernière
clause, qui tente de lire un nombre et sert de cas général. L'accumulateur
`acc` garde les tokens à l'envers, puis `Enum.reverse/1` les remet dans
l'ordre, une seule fois, à la fin.

> 💡 `defp` définit une fonction privée, appelée seulement depuis son module :
> `scan/2` est notre boucle, elle n'a pas besoin d'être publique. Le garde
> `when space in [32, 9, 10, 13]` affine le motif ; `in` teste l'appartenance
> à une liste. Le motif `<<space, rest::binary>>` sépare le premier octet de la
> chaîne de tous les suivants. Les blancs que nous cherchons sont des octets
> ASCII : 32 pour l'espace, 9 pour la tabulation, 10 et 13 pour les fins de
> ligne. Le motif `"+" <> rest`, lui, reconnaît un préfixe, comme en AP1.
>
> `with {:ok, value} <- operation() do suite end` n'exécute `suite` que si le
> résultat correspond au motif ; un `{:error, raison}` qui ne correspond pas
> est renvoyé tel quel, sans rien exécuter d'autre. Ici, lire un nombre avec
> succès fournit aussi le reste de la chaîne, sur lequel le parcours continue ;
> un échec de lecture devient directement le résultat du lexer.

```elixir
defmodule ArithmeticInput do
  def lex(source), do: scan(source, [])
  defp scan("", acc), do: {:ok, Enum.reverse(acc)}
  defp scan(<<space, rest::binary>>, acc) when space in [32, 9, 10, 13], do: scan(rest, acc)
  defp scan("+" <> rest, acc), do: scan(rest, [:plus | acc])
  # Ajoutez ici vos clauses pour les autres symboles.
  defp scan(source, acc) do
    with {:ok, number, rest} <- DecimalLiteral.read(source) do
      scan(rest, [{:number, number} | acc])
    end
  end
end
```

Les vérifications de ce notebook utilisent `assert`, fourni par Elixir via
ExUnit : une condition fausse arrête la cellule et affiche la valeur obtenue
à côté de celle attendue. Évaluez la cellule d'import une fois, puis
réévaluez votre code avant ses tests après chaque modification.

```elixir
import ExUnit.Assertions
```

Prédisez les tokens de `"12.0 + 3.0 * 2.0"` avant d'évaluer. La forme
`assert {:error, _} = résultat` vérifie seulement que le résultat est une
erreur, quelle qu'en soit la raison : le motif `_` accepte n'importe quoi.
Pourquoi `"1.0e2"` doit-il être refusé, alors que Python l'accepterait ?

```elixir
assert ArithmeticInput.lex("12.0 + 3.0 * 2.0") ==
  {:ok, [{:number, 12.0}, :plus, {:number, 3.0}, :multiply, {:number, 2.0}]}
assert {:error, _} = ArithmeticInput.lex("1.0 + @")
assert {:error, _} = ArithmeticInput.lex("1.0e2")
```

## Décider quel état transporter

Les opérations sur deux valeurs vous sont fournies ci-dessous.
`ArithmeticOps.apply(op, left, right)` rend `{:ok, résultat}` ou
`{:error, raison}` : elle refuse les valeurs qui ne sont pas des flottants
(c'est le rôle du garde `is_float/1`) et signale une division par zéro. Elle
ne décide ni des priorités ni de l'ordre de lecture ; c'est le rôle du
calculateur que vous allez écrire.

> 💡 Sur la BEAM, un calcul flottant qui dépasse la plus grande valeur
> représentable lève une exception `ArithmeticError`. Le `try … rescue`
> de `apply/3` la transforme en `{:error, :arithmetic_overflow}`, pour rester
> dans le contrat des résultats étiquetés. `defp numeric(:divide, _left, right)
> when right == 0.0` est testée avant la division ordinaire : l'ordre des
> clauses fait le travail.

```elixir
defmodule ArithmeticOps do
  def negate(value) when is_float(value), do: {:ok, -value}
  def negate(_value), do: {:error, :expected_number}
  def apply(op, left, right) when is_float(left) and is_float(right) do
    try do
      numeric(op, left, right)
    rescue
      ArithmeticError -> {:error, :arithmetic_overflow}
    end
  end
  def apply(_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(_op, _left, _right), do: {:error, :unknown_operator}
end
```

Concevez `DirectCalculator.calculate/1` à partir d'une liste de tokens. Avant
d'écrire une ligne, dessinez l'état après chaque lecture de `8.0 - 3.0 - 2.0`.
Les deux premières lignes du tableau sont données ; à vous de compléter les
suivantes :

| Tokens restant à lire | Résultat déjà obtenu |
|---|---|
| `[{:number, 8.0}, :minus, {:number, 3.0}, :minus, {:number, 2.0}]` | aucun |
| `[:minus, {:number, 3.0}, :minus, {:number, 2.0}]` | `8.0` |
| … | … |

Chaque appel récursif doit donc transporter deux informations : ce qui reste
à lire et le résultat courant. C'est le couple « reste à traiter, travail
déjà fait » de vos accumulateurs d'AP1. Écrivez ensuite l'algorithme entier.

Contrat : `{:ok, nombre}` si toute l'entrée respecte notre règle, sinon
`{:error, raison}`. Propagez notamment `{:error, :division_by_zero}` telle que
`ArithmeticOps` la rend. Vous choisissez les noms des fonctions auxiliaires et
des autres raisons d'erreur.

Les petits exemples `iex>` décrivent une entrée et le résultat attendu, au
format *doctest* : la ligne `iex>` est l'appel, la ligne suivante la valeur
rendue. Ils restent du texte dans Livebook ; les cellules `assert` ci-dessous
vérifient votre code sans configuration supplémentaire.

```text
iex> DirectCalculator.run("1.0 + 2.0 * 3.0")
{:ok, 9.0}
```

```elixir
defmodule DirectCalculator do
  def run(source) do
    with {:ok, tokens} <- ArithmeticInput.lex(source), do: calculate(tokens)
  end
  def calculate(_tokens), do: {:error, :not_implemented}
  # Remplacez cette fonction et ajoutez vos fonctions auxiliaires.
end
```

Avant de lancer les vérifications, prédisez la ligne qui échouera si votre
calculateur accepte un préfixe valide sans regarder la suite, par ex.
`"1.0 2.0"`.

> 💡 `for source <- exemples do … end` parcourt la liste : le nom `source`
> reçoit chaque valeur à son tour, et le corps est exécuté pour chacune. Nous
> l'utilisons seulement pour répéter la même vérification ; `for` construit
> aussi une liste de résultats, que nous ignorons ici. Nous l'étudierons en AP4.

```elixir
assert DirectCalculator.run("8.0 - 3.0 - 2.0") == {:ok, 3.0}
assert DirectCalculator.run("1.0 + 2.0 * 3.0") == {:ok, 9.0}
assert DirectCalculator.run("8.0 / 2.0 / 2.0") == {:ok, 2.0}
assert DirectCalculator.run("1.0 / 0.0") == {:error, :division_by_zero}
for source <- ["", "1.0 +", "1.0 2.0", "(1.0 + 2.0)", "-1.0"] do
  assert {:error, _} = DirectCalculator.run(source)
end
```

### Si vous bloquez

Premier indice : le premier nombre initialise l'état. Deuxième indice : la suite
attendue alterne un opérateur et un nombre. Une récursion peut consommer les deux
à la fois, avec un motif `[op, {:number, right} | rest]` ; une liste vide
signifie alors que le calcul est terminé. Troisième indice : tout ce qui ne
correspond ni à la fin ni à cette paire est une erreur.

### Pour aller plus loin

Inventez une expression qui distingue notre règle de la priorité habituelle.
Peut-on retrouver l'expression initiale à partir du seul résultat ? Qu'aurions-nous
besoin de conserver pour afficher, transformer ou exécuter le calcul plus tard ?

## Avant de reprendre les slides

Expliquez pourquoi le deuxième exemple donne `9.0` et quelle règle il faudrait
changer pour obtenir `7.0`. L'absence d'arbre n'interdit pas en soi de gérer
priorités et parenthèses : c'est notre **algorithme de lecture** qui est limité.
Décrivez une piste possible, puis gardez une trace de votre contre-exemple.
Retenez enfin ce que cette étape a mis en place : un lexer qui rend des
tokens ou une erreur, des opérations qui rendent des résultats étiquetés, et
un parcours qui transporte le reste à lire avec le résultat courant.

Écrivez vos réponses dans une cellule Markdown de ce notebook, avec votre
contre-exemple et ce qu'il vous a appris. Sauvegardez ce notebook dans `AP2/`.
Pour le rendu unique du cycle AP1–AP3, je préciserai en cours les étapes
à terminer et le résultat minimum attendu. Les numéros indiquent l'ordre
de progression, pas une liste de devoirs. Les prolongements non demandés
restent facultatifs et peuvent apporter un bonus.
L'échéance commune est le 6 décembre 2026 à 23 h 59 (Paris) ; il n'y a pas
de devoir intermédiaire. AP3 s'ouvrira par un quiz de dix minutes sur
cette séance : relisez le chapitre 5 du poly, « Construire un langage ».

Si un bout de syntaxe Elixir vous manque en route, les chapitres 1 à 4 du
[poly](https://ap-2026.noce.fr/poly/poly-elixir.pdf) se consultent au besoin ; ils ne sont pas un prérequis de cette
étape.

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