AP · Découverte
Ce que nous allons construire
Du code aux pixels.
Des exemples à l’intelligence.
À vous de construire la suite.
Aujourd’hui, on essaie les résultats. En cours, on en construit les briques.
AP1 · Premiers pas en Elixir
Du code que vous pouvez déjà lire
Pouvez-vous deviner le résultat sans avoir appris Elixir ?
prix = 12
quantite = 3
total = prix * quantite
"Total : #{total} €"AP1 · Fonctions et motifs
Décrire chaque cas directement
Quelle ligne correspond à un feu vert ?
defmodule Feu do
def action(:rouge), do: "Stop"
def action(:orange), do: "Attention"
def action(:vert), do: "On avance"
end
Feu.action(:vert)AP4 · Collections et pipe
Lire les transformations dans l’ordre
Garder les notes validées, puis les classer de la plus haute à la plus basse.
notes = [8, 14, 11, 6, 17]
notes
|> Enum.filter(fn note ->
note >= 10
end)
|> Enum.sort()
|> Enum.reverse()Ces trois extraits sont en Elixir. Nous allons maintenant essayer un petit langage différent, dont nous écrirons l’interpréteur en Elixir.
AP2–AP3 · Langages
Donner un sens à un programme
Que peut-on savoir avant même de lancer un programme ?
Nous lirons ce texte pour construire un arbre, puis calculer sa valeur.
Modifiez un nombre, puis lancez le programme.
Voir l’arbre construit à partir du texte
Lecture du haut vers le bas : les liens indiquent le rôle de chaque enfant. Cliquez sur un nœud pour replier ou déplier ses enfants ; faites défiler la vue pour explorer l’arbre.
L’arbre apparaîtra après l’analyse du programme.
Voir aussi les tuples Elixir
L’arbre apparaîtra après l’analyse du programme.
Parser, typeur et interpréteur Elixir du cours, exécutés dans votre navigateur. Premier chargement : environ 11 Mo, avec les modules déjà compilés. Voir la référence exécutée.
AP3 et AP9 · Machines
Du programme aux pixels
Ce jeu peut-il tenir dans les instructions d’une toute petite machine ?
Chargement de la ROM compilée…
Cliquez sur l’écran, puis utilisez les flèches. Poussez la caisse sur le point ; vous ne pouvez pas la tirer.
Voir la tilemap · les 7 motifs de tile
tile(x, y, indice) dessine l’un de ces motifs de 4 × 4 pixels. Les coordonnées x et y placent son coin supérieur gauche sur l’écran.
0Mur1Sol vide2Objectif3Caisse4Caisse sur un objectif5Joueur6Joueur sur un objectifPar ex., tile(20.0, 6.0, 3.0) dessine une caisse en (20, 6). Les motifs sont fournis ; draw_cell choisit leur indice selon l’état du jeu.
Le dessin utilise un XOR : redessiner le même motif au même endroit l’efface. Le sol vide ne modifie aucun pixel ; ici, main() efface l’écran avec clear() avant de redessiner le plateau.
Que fait le processeur ?
Activez « Pas à pas », puis appuyez sur une flèche du jeu. Avancez d’une instruction pour voir les valeurs changer, ou continuez jusqu’à la prochaine attente de touche.
Le programme en mémoire
— octets—Processeur CHIP-8
Une instruction à la fois——16 registres : de petites cases pour calculer
VideAucuneMémoire du jeu
À partir de —
Convention du programme initial : positions dans la carte ; victoire : 0 ou 1. Le processeur ne connaît pas ces rôles.
Clavier et écran
Aucune0 / 2 048En attente d’une touche, la machine n’exécute plus d’instructions.
Couleur : valeur différente du dernier état affiché.
Adresses et instructions en hexadécimal (0x…) ; valeurs des registres et du jeu en décimal. Le PC indique où lire la prochaine instruction.
Le jeu complet est dans sokoban.ap : carte, fonctions de déplacement et de dessin, initialisation et boucle main(). Les onglets Elixir montrent le compilateur utilisé pour produire la ROM.
Génération de code · Elixir · lecture seule. Ce fichier traduit les expressions, les appels de fonctions et les opérations de la machine en instructions CHIP-8.
defmodule PresentationChip8.Codegen do
@moduledoc "Expressions en V0, fonctions à cases locales statiques et auto-appels terminaux en sauts."
def initial(functions) do
parameters =
Map.new(functions, fn function ->
{function.name, Enum.map(function.parameters, &{:parameter, function.name, &1})}
end)
slots =
for function <- functions,
label <- Map.fetch!(parameters, function.name),
do: {:data, label, <<0>>}
%{next: 0, slots: slots, parameters: parameters, current: nil}
end
def fresh(state, kind), do: {{kind, state.next}, %{state | next: state.next + 1}}
def slot(state) do
{label, state} = fresh(state, :slot)
{label, %{state | slots: [{:data, label, <<0>>} | state.slots]}}
end
def function(definition, state) do
env = Map.new(Enum.zip(definition.parameters, Map.fetch!(state.parameters, definition.name)))
{code, state} = emit(definition.body, env, %{state | current: definition.name})
{[{:label, {:function, definition.name}}] ++ code ++ [:return], %{state | current: nil}}
end
def emit({:number, n}, _env, state), do: {[{:load, 0, trunc(n)}], state}
def emit({:bool, b}, _env, state), do: {[{:load, 0, if(b, do: 1, else: 0)}], state}
def emit({:name, name}, env, state), do: {read(Map.fetch!(env, name)), state}
def emit({:neg, value}, env, state), do: emit({:binary, :-, {:number, 0}, value}, env, state)
def emit({:binary, op, left, {:number, power}}, env, state) when op in [:*, :/] do
{code, state} = emit(left, env, state)
shifts = shift_count(trunc(power))
instruction = if op == :*, do: {:add, 0, 0}, else: {:raw, 0x8006}
{code ++ List.duplicate(instruction, shifts), state}
end
def emit({:binary, op, left, right}, env, state) do
{left_code, state} = emit(left, env, state)
{saved, state} = slot(state)
{right_code, state} = emit(right, env, state)
{operation, state} = operation(op, state)
{left_code ++ write(saved) ++ right_code ++ [{:copy, 1, 0}] ++ read(saved) ++ operation,
state}
end
def emit({:let, name, value, body}, env, state) do
{value_code, state} = emit(value, env, state)
{saved, state} = slot(state)
{body_code, state} = emit(body, Map.put(env, name, saved), state)
{value_code ++ write(saved) ++ body_code, state}
end
def emit({:case, condition, yes, no}, env, state) do
{condition_code, state} = emit(condition, env, state)
{yes_label, state} = fresh(state, :yes)
{done, state} = fresh(state, :done)
{yes_code, state} = emit(yes, env, state)
{no_code, state} = emit(no, env, state)
{condition_code ++
[{:skip_eq, 0, 0}, {:jump, yes_label}] ++
no_code ++
[{:jump, done}, {:label, yes_label}] ++ yes_code ++ [{:label, done}], state}
end
def emit({:call, {:name, name}, args}, env, state) do
if Map.has_key?(state.parameters, name) do
{argument_code, saved, state} = arguments(args, env, state)
assignment =
Enum.zip(saved, Map.fetch!(state.parameters, name))
|> Enum.flat_map(fn {source, target} -> read(source) ++ write(target) end)
call =
if name == state.current, do: {:jump, {:function, name}}, else: {:call, {:function, name}}
{argument_code ++ assignment ++ [call], state}
else
primitive(name, args, env, state)
end
end
defp read(label), do: [{:index, label}, {:fetch, 0}]
defp write(label), do: [{:index, label}, {:store, 0}]
defp shift_count(1), do: 0
defp shift_count(n), do: 1 + shift_count(div(n, 2))
defp arguments(args, env, state) do
Enum.reduce(args, {[], [], state}, fn arg, {code, saved, state} ->
{value_code, state} = emit(arg, env, state)
{slot, state} = slot(state)
{code ++ value_code ++ write(slot), saved ++ [slot], state}
end)
end
defp operation(:+, state), do: {[{:add, 0, 1}], state}
defp operation(:-, state), do: {[{:sub, 0, 1}], state}
defp operation(:<, state),
do: {[{:sub, 0, 1}, {:copy, 1, 15}, {:load, 0, 1}, {:sub, 0, 1}], state}
defp operation(:==, state) do
{done, state} = fresh(state, :equal)
{[
{:load, 2, 0},
{:skip_reg_eq, 0, 1},
{:jump, done},
{:load, 2, 1},
{:label, done},
{:copy, 0, 2}
], state}
end
defp primitive("clear", [], _env, state), do: {[:clear, {:load, 0, 0}], state}
defp primitive("read_key", [], _env, state), do: {[{:key, 0}], state}
defp primitive("rem", [value, {:number, power}], env, state) do
{code, state} = emit(value, env, state)
{code ++ [{:load, 1, trunc(power) - 1}, {:raw, 0x8012}], state}
end
defp primitive("load", [address], env, state) do
{code, state} = emit(address, env, state)
{code ++ [{:index, :memory}, {:raw, 0xF01E}, {:fetch, 0}], state}
end
defp primitive("store", [address, value], env, state) do
{code, [saved_address, saved_value], state} = arguments([address, value], env, state)
{code ++
read(saved_value) ++
[{:copy, 1, 0}] ++
read(saved_address) ++
[{:index, :memory}, {:raw, 0xF01E}, {:copy, 0, 1}, {:store, 0}, {:load, 0, 0}], state}
end
defp primitive("tile", [x, y, sprite], env, state) do
{code, [saved_x, saved_y, saved_sprite], state} = arguments([x, y, sprite], env, state)
{valid, state} = fresh(state, :tile_valid)
{done, state} = fresh(state, :tile_done)
code =
code ++
read(saved_sprite) ++
[
{:copy, 4, 0},
{:load, 1, 7},
{:sub, 0, 1},
{:skip_eq, 15, 1},
{:jump, valid},
{:load, 0, 0},
{:jump, done},
{:label, valid},
{:copy, 0, 4},
{:add, 0, 0},
{:add, 0, 0},
{:copy, 4, 0}
] ++
read(saved_x) ++
[{:copy, 2, 0}] ++
read(saved_y) ++
[
{:copy, 3, 0},
{:index, {:sprite, 0}},
{:raw, 0xF41E},
{:draw, 2, 3, 4},
{:load, 0, 0},
{:label, done}
]
{code, state}
end
end
Compilateur · Elixir · lecture seule. Ce fichier analyse et vérifie le programme, puis assemble la ROM. Les règles du Sokoban sont dans sokoban.ap.
defmodule PresentationChip8 do
@moduledoc "Compiler le langage u8 à fonctions fermées ; aucune règle de jeu dans le compilateur."
alias PresentationChip8.{Assembler, Codegen, Lexer, Parser, Source, Sprites}
def compile(text) when is_binary(text) and byte_size(text) <= 16000 do
with {:ok, tokens} <- Lexer.lex(text),
{:ok, parsed} <- Parser.parse(tokens),
{:ok, program} <- Source.validate(normalize(parsed)) do
state = Codegen.initial(program.functions)
{entry, state} = Codegen.emit(program.entry, %{}, state)
{functions, state} = Enum.map_reduce(program.functions, state, &Codegen.function/2)
instructions =
entry ++
[{:label, :halt}, {:jump, :halt}] ++
List.flatten(functions) ++
Sprites.data() ++
Enum.reverse(state.slots) ++ [{:data, :memory, <<0::2048>>}]
with {:ok, artifact} <- Assembler.assemble(instructions) do
{:ok, Map.merge(artifact, %{type: program.type, memory: artifact.labels.memory})}
end
end
rescue
error -> {:error, Exception.message(error)}
end
def compile(_), do: {:error, "Le programme doit contenir au plus 16 000 octets."}
defp normalize({:binary, op, left, right}) do
mapped = %{plus: :+, minus: :-, multiply: :*, divide: :/, lt: :<, eq: :==}
{:binary, Map.get(mapped, op, op), normalize(left), normalize(right)}
end
defp normalize({:number, number}) when is_number(number) do
{:number, if(trunc(number) == number, do: trunc(number), else: number)}
end
defp normalize(tuple) when is_tuple(tuple),
do: tuple |> Tuple.to_list() |> Enum.map(&normalize/1) |> List.to_tuple()
defp normalize(list) when is_list(list), do: Enum.map(list, &normalize/1)
defp normalize(other), do: other
@doc "Protocole JSON du runner, sans dépendance à Jason."
def browser(text) do
case compile(text) do
{:ok, artifact} ->
bytes = artifact.rom |> :binary.bin_to_list() |> Enum.join(",")
IO.puts(
"@@CHIP8@@{\"rom\":[" <>
bytes <>
"],\"memory\":" <>
Integer.to_string(artifact.memory) <>
",\"type\":\"" <> Atom.to_string(artifact.type) <> "\"}"
)
:ok
{:error, reason} ->
raise ArgumentError,
"Compilation refusée : #{if is_binary(reason), do: reason, else: inspect(reason)}"
end
end
end
La ROM initiale est prête à jouer. « Compiler et jouer » compile uniquement sokoban.ap, avec le compilateur Elixir.
Quelles opérations sont fournies par la machine ?
Le programme définit les fonctions propres au jeu. Six primitives lui donnent accès à l’écran, au clavier, à la mémoire et au calcul de reste. Leur traduction est visible dans codegen.ex. Un u8 est un entier de 0 à 255 ; les calculs débordent modulo 256.
clear() → u8- Efface l’écran et renvoie 0.
read_key() → u8- Attend une touche du clavier CHIP-8. Les flèches de la démo envoient 2, 4, 6 et 8.
tile(x: u8, y: u8, indice: u8) → u8- Dessine une tuile de 4 × 4 pixels par XOR aux coordonnées indiquées, puis renvoie 0. Indices de 0 à 6 ; hors de cet intervalle, ne dessine rien.
load(adresse: u8) → u8- Lit un octet dans les 256 cases de mémoire réservées au programme.
store(adresse: u8, valeur: u8) → u8- Écrit un octet dans cette mémoire et renvoie 0. Le programme décide ce que chaque case représente.
rem(valeur: u8, modulo: u8) → u8- Calcule un reste. Ce backend accepte un modulo constant égal à une puissance de deux.
La fonction main redessine le jeu, attend une touche, applique les règles, puis se rappelle. Ce dernier appel est compilé en saut : la pile ne grandit pas à chaque tour.
Positions, sources et ROM
Le parser vient d’AP2 et le backend entier sur 8 bits d’AP9. Cette démonstration étend le backend avec des fonctions nommées et des appels, dont l’auto-appel terminal de main.
Les positions comptent huit cases par ligne. Sur 8 bits, ajouter 255 revient à reculer d’une case ; ajouter 248 revient à monter d’une ligne. L’émulateur de cette présentation est en JavaScript. Le projet backend d’AP9 fait compiler le programme vers ses instructions.
Le jeu choisit où ranger son état : case mémoire 0 pour le joueur, 1 pour la caisse, 2 pour la cible, 3 pour la victoire. Les nombres de l’initialisation se trouvent juste avant le dernier appel à main().
AP4–AP5 → AP9 · Image par coordonnées
Cette image est un réseau de neurones
Donnez-lui une position. Il invente la couleur qui va avec.
— paramètresErreur validation : —
Le réseau a appris les couleurs de cette image. Aucune couleur de l’original n’est consultée pour la reconstruire.
Corrigé AP9 « Image par coordonnées », adapté à la couleur. Vrais poids entraînés avec Nx ; le curseur rejoue des étapes enregistrées.
Comprendre l’apprentissage sur quatre points
Fond plus bleu : prédiction +1.
— / 4 points correctement classés
Calculs réels dans le navigateur, vérifiés contre le corrigé Elixir de micrograd. Ces quatre points servent à apprendre.
AP9 · Autoencodeur
Abîmez le dessin. Le réseau le reconstruit.
Que peut-il retrouver à partir de ce qui reste ?
Dessinez, effacez, recommencez.
Recalculé à chaque geste
Et entre deux dessins ? Explorer le code appris
Le réseau décode les nombres intermédiaires. Un mélange peut donner un chiffre ambigu.
Chargement du modèle entraîné en Elixir…
Autoencodeur d’AP9, adapté à MNIST : 12 000 chiffres d’apprentissage. Les exemples ci-dessus viennent d’un lot distinct.
AP9 · Petit Transformer
À quoi le Transformer fait-il attention ?
Retrouver les bonnes lettres, une prédiction après l’autre.
Choisissez trois lettres parmi a, b, c et d. Les deux réseaux reçoivent le même défi.
Avant d’apprendre
0 pasDes poids aléatoires.
Après apprentissage
1 200 pasLes mêmes briques, des poids ajustés.
Comment choisit-il la première lettre ?
Contexte disponible Futur inaccessible^ début · | séparateur · ↵ fin
Chargement des poids du petit Transformer.
Explorer la carte d’attention et les règles de l’expérience
Chaque ligne de la carte montre les poids vers les positions déjà disponibles. Les cases hachurées sont masquées : une position ne voit pas la suite.
À température 0, on choisit le caractère le plus probable. Au-dessus, on tire au sort selon les probabilités recalculées : une température plus élevée les étale. « Rejouer » effectue de nouveaux tirages.
L’attention n’explique qu’une partie du calcul : le modèle utilise aussi des connexions résiduelles et un réseau dense.
Une tête d’attention, un bloc, sept caractères.
Vrais poids appris avec Nx · calcul local dans votre navigateur · une tâche de copie, pas un modèle de français. Le projet AP9
AP6 → AP9 · Ferme de rendu
Une image. Plusieurs travailleurs.
Si un travailleur s’arrête, doit-on perdre toute l’image ?

La fractale est calculée ici. Lancez le rendu, puis provoquez une panne : que deviennent les autres tuiles ?
- En attente du premier résultat.
Workers du navigateur ; projet en processus Elixir. Même idée : distribuer des travaux identifiés, recueillir les réponses et détecter les pannes. Délais de livraison ajoutés pour observer les messages ; ce n’est pas un benchmark.
AP7 → AP9 · Réservations concurrentes
Vingt demandes. Une seule place.
Comment éviter de promettre la dernière place à deux personnes ?
- Jeton propriétaire
- —
- Demandes acceptées
- 0 / 20
- Demandes refusées
- 0
Une place, un seul propriétaire.
23 billets vendus, une place libre. Qui l’obtiendra si 20 clients la demandent ?
Observer les messages et la mémoire du serveur
Protocole simulé dans le navigateur ; GenServer dans le projet Elixir. Chaque demande vérifie et modifie la place dans un seul traitement. Le temps avance ici avec les boutons. La supervision redémarre le processus ; elle ne restaure pas ses réservations en mémoire.
AP6–AP8 → AP9 · Éditeur concurrent
Deux personnes. Un même texte.
Écrivez des deux côtés, coupez la connexion, puis retrouvez-vous.
Alice
r0Bob
r0Chacun a écrit sans recevoir l’autre. Que va devenir la position de l’ajout de Bob ?
Voir comment les positions changent
Vous pouvez aussi écrire dans les deux éditeurs. Cette simulation accepte du texte ASCII court (sans accents ni emoji). Les échanges sont ordonnés par un serveur ; une coupure retient les messages en mémoire.
Le calcul de transformation suit le corrigé du projet OT d’AP9, avec accusés et composition des saisies en attente. Les positions commencent à zéro.
Revenir au compteur répliqué d’AP8 · CRDT
Alice
contribution(s) connues
- Alice
- 0
- Bob
- 0
- Carol
- 0
Bob
contribution(s) connues
- Alice
- 0
- Bob
- 0
- Carol
- 0
Carol
contribution(s) connues
- Alice
- 0
- Bob
- 0
- Carol
- 0
Aucun message gardé.
Les échanges sont suspendus. Chacun peut ajouter des contributions.
À la réception, chaque composante garde le maximum : max(local, reçu).
AP9 · Affectation et algorithmique
Une place pour chacun ?
Les mêmes préférences peuvent-elles conduire à des résultats différents ?
Les règles : quels vœux, quelles priorités ?
Chaque élève essaie ses groupes de gauche à droite. Les trois groupes utilisent la même priorité fictive : . Une acceptation provisoire peut être remplacée par celle d’un élève plus prioritaire.
Une paire « élève–groupe » bloque une affectation si l’élève préfère ce groupe et si le groupe pourrait l’accepter à la place d’un candidat moins prioritaire (ou dans une place libre). Aucune paire bloquante : l’affectation est stable. Cela ne garantit ni de placer tout le monde, ni de minimiser la somme des rangs.
Algorithmes du projet Elixir AP9 « Affectation de groupes », adaptés ici en JavaScript et comparés au corrigé. Les six élèves sont fictifs. Le nombre maximal de places occupables est calculé ici par recherche exhaustive ; le TP le calcule avec un flot.
Neuf séances
Construire par petites étapes
- AP1–AP3
Décrire et exécuter
Elixir, les données, les fonctions.
Dans Livebook
Un interpréteur, puis un compilateur. - AP4–AP5
Calculer et apprendre
Des tenseurs, un gradient.
Dans Livebook
Des modèles et des expériences. - AP6–AP8
Faire coopérer
Des processus et des messages.
Vers des applications Elixir
Un état local, puis répliqué. - AP9
Approfondir une idée
Un projet commencé en cours.
Un sujet au choix
Une réalisation poursuivie à la maison.
Prédire. Écrire. Essayer. Expliquer.
Un bout de code que vous comprenez, un exemple qui le vérifie, puis la prochaine brique.
AP9 · Projet au choix
Choisir une question à approfondir
Langages et machines
IA et apprentissage
Concurrence et systèmes répartis
Algorithmique et optimisation
Les interfaces, les données et les squelettes sont fournis. Vous travaillez sur l’idée et son algorithme.
À vous de jouer
Vous écrirez les briques qui font fonctionner tout cela
Le programme qui tourne est le point de départ d’une question :
comment fonctionne-t-il ?
Le carnet réunira nos explications, votre code et ses résultats.
Pour refaire les essaisap-2026.noce.fr/presentationLes démonstrations restent accessibles depuis cette page.
Quel essai voulez-vous refaire ?
LangageSokobanImage appriseChiffresTransformerFerme de renduRéservationsÉditeursGroupes