Erreurs et débogage
Lire un message d'erreur, corriger les plus courantes, lever et rattraper vos propres erreurs, et traquer les bugs avec des traces, des fichiers journaux et l'invite brass.
Tout programme finit par se tromper : une faute de frappe, un coffre déplacé, un nombre lu comme du texte. En Brass, une erreur n'abîme jamais l'ordinateur ni le monde. Le programme s'arrête, et un court message dit où et pourquoi. Ce guide vous apprend à lire ces messages, à corriger les suspects habituels, à lever et rattraper vos propres erreurs, et à trouver les bugs qui ne s'annoncent pas.
Lire un message d'erreur
Quand un programme rencontre une erreur, il s'arrête aussitôt. Le message s'affiche en rouge sous ce que le programme avait écrit, et l'invite revient :
> porte Ouverture de la barrière nord... porte:12: attempt to index a nil value (local 'lampe') >
Le message a trois parties, séparées par des deux-points :
- Le fichier :
porte. Un fichier rangé dans un dossier montre son chemin complet (apps/porte/main:12:), une ligne tapée à l'invitebrasss'appellebrass, et sur ce site les exemples s'appellentsnippet. Quand un programme est découpé en plusieurs fichiers avecimport(), c'est ce qui vous dit lequel ouvrir. - La ligne :
12. Tapezedit porte: l'éditeur affiche les numéros de ligne dans la marge. - Ce qui s'est mal passé :
attempt to index a nil value (local 'lampe')(« tentative d'indexer une valeur nil »). La partie entre parenthèses désigne le coupable quand Brass le connaît : ici, la variable localelampevalaitnil.
Les messages de la machine sont en anglais : cette page explique les plus courants, et Messages d'erreur les liste tous.
Brass ne montre que la ligne où l'erreur s'est produite, pas la chaîne d'appels qui y a mené. Si la ligne est dans une fonction appelée depuis plusieurs endroits, ajoutez un print avant l'appel (voir le débogage) pour savoir de quel appel il s'agit.
Une erreur arrête le programme, pas ce qu'il a déjà fait. Une sortie redstone allumée reste allumée, une presse mécanique en marche continue de tourner. Si c'est important, rattrapez l'erreur et mettez les machines dans un état sûr (voir pcall), ou redémarrez avec Ctrl+R : un redémarrage coupe toutes les sorties.
Erreurs de compilation et erreurs d'exécution
Une erreur peut apparaître à deux moments.
Les erreurs de compilation arrivent avant le démarrage du programme. Brass lit d'abord le fichier entier et le traduit en instructions pour le processeur. Si le texte n'est pas du Brass valide (un end manquant, un symbole égaré, une chaîne pas refermée), rien ne s'exécute, pas même la première ligne. Le message dit où le compilateur s'est perdu, ce qui n'est pas toujours l'endroit de la faute : un end manquant n'est souvent remarqué qu'à la fin du fichier.
local niveau = 12
if niveau > 10 then
print("trop haut")snippet:3: 'end' expected (to close 'if' at line 2) near <eof>
Ici la faute est ligne 2 (le if n'a pas de end), mais le compilateur ne l'a su qu'à la fin du fichier, ligne 3, où <eof> (end of file, la fin du fichier) est arrivé à la place de end. Le message donne la ligne d'ouverture pour vous aider.
Les erreurs d'exécution arrivent pendant que le programme tourne : tout ce qui précède la ligne fautive a été fait. Elles dépendent des valeurs du moment, si bien qu'un programme peut tourner une heure sans souci puis échouer quand un coffre se vide ou qu'un bloc est cassé.
Les erreurs d'exécution les plus courantes
attempt to index a nil value
Vous lisez un champ (x.nom, x[1], x:methode()) sur quelque chose qui vaut nil.
local texte = fs.read("reglages") -- nil : ce fichier n'existe pas
print(texte:upper())snippet:2: attempt to index a nil value (local 'tex te')
Les parenthèses disent quelle valeur était nil : local 'texte', global 'config', ou field 'alarme' quand c'était le champ d'une table :
local reglages = {gare = "Mine de fer"}
print(reglages.alarme.cote)snippet:2: attempt to index a nil value (field 'ala rme')
reglages.alarme n'existe pas, donc on ne peut pas y lire .cote. Causes habituelles :
- une fonction qui renvoie
nilquand elle ne trouve rien :fs.read(pas de fichier),peripheral.wrapetperipheral.find(pas de bloc à cet endroit),net.receive(délai écoulé),string.find(pas trouvé) ; - une faute de frappe dans un nom de champ (
reglages.Garen'est pasreglages.gare) ; - une bibliothèque que cet ordinateur n'a pas :
gfxsur le Calculateur à tubes,netavant le Mini-ordinateur. Le message est alorsattempt to index a nil value (global 'net').
Correction : testez avant d'utiliser, et dites ce qui manque.
local lampe = peripheral.wrap("top")
if lampe == nil then
error("pas de lampe sur le dessus de l'ordinateur")
endattempt to call a nil value
Vous avez appelé quelque chose qui n'est pas une fonction : un nom mal écrit, une fonction d'une autre API, ou une fonction définie plus bas.
term.setCursorPos(1, 1)snippet:1: attempt to call a nil value (field 'setC ursorPos')
term n'a pas de setCursorPos (c'est le nom de ComputerCraft) : Brass l'appelle term.set_cursor. Les fonctions de Brass s'écrivent en snake_case, voir Brass pour les habitués de Lua et ComputerCraft.
Le code d'un fichier s'exécute de haut en bas, et function saluer() crée saluer au moment où cette ligne s'exécute. L'appeler avant échoue :
saluer("Steve")
function saluer(nom)
print("Bonjour, " .. nom)
endsnippet:1: attempt to call a nil value (global 'sal uer')
Correction : définissez vos fonctions en haut du fichier, et lancez le travail en bas. Quand le nom est juste et que la fonction existe, vérifiez l'orthographe de sa bibliothèque (string.split, pas string.Split), et tapez help term à l'invite pour lister ce qu'une bibliothèque contient.
attempt to perform arithmetic on a nil value (ou a string value)
Un +, -, *, /, % ou ^ a reçu autre chose qu'un nombre.
local stock = {fer = 120}
print(stock.fer + stock.cuivre)snippet:2: attempt to perform arithmetic on a nil v alue
Ce message ne nomme pas la valeur : regardez chaque opérande de la ligne. Ici, stock.cuivre n'existe pas. Donnez une valeur par défaut aux valeurs absentes avec or : stock.fer + (stock.cuivre or 0).
Avec a string value, le nombre est en fait du texte, en général lu avec read ou fs.read. Brass ne convertit jamais de lui-même un texte en nombre : utilisez tonumber, et vérifiez nil (voir Travailler avec du texte).
attempt to concatenate a nil value
L'opérateur .. n'assemble que du texte et des nombres.
local arrets = {"Mine de fer", "Usine de laiton"}
print("Prochain arrêt : " .. arrets[3])snippet:2: attempt to concatenate a nil value
Même chose avec a boolean value ("allumé : " .. true) et a table value. Correction : tostring(valeur), ou une valeur par défaut : (arrets[3] or "terminus").
attempt to compare
<, >, <= et >= comparent deux nombres ou deux chaînes, jamais un mélange :
local niveau = "12" -- lu dans un fichier : c'est du texte
if niveau > 10 then
print("trop haut")
endsnippet:2: attempt to compare string with number
Autres formes : attempt to compare nil with number (une valeur absente), attempt to compare two table values. Correction : tonumber(niveau). (== et ~= n'échouent jamais : "12" == 12 vaut simplement false.)
bad argument
Une fonction de bibliothèque a reçu une valeur dont elle ne peut rien faire. Le message donne la position de l'argument, le nom de la fonction, ce qu'elle attendait (expected) et ce qu'elle a reçu (got) :
local tape = "3.7"
print(math.floor(tape))snippet:2: bad argument #1 to 'floor' (number expec ted, got string)
Un argument manquant, ou nil, s'affiche got no value :
print(string.rep("-"))snippet:1: bad argument #2 to 'rep' (number expecte d, got no value)
Certaines fonctions s'expliquent avec leurs propres mots : (number has no integer representation) quand il faut un nombre entier et que vous avez donné 2.5, (color must be 0..15) pour term.set_fg, bad side 'up' (front, back, left, right, top, bottom) pour rs.set. Tous les messages sont listés dans Messages d'erreur.
Autres messages que vous croiserez
| message | cause | correction |
|---|---|---|
attempt to get length of a nil value | #x alors que x vaut nil | vérifier que la table existe |
attempt to index a string value with a number key | nom[1] pour avoir un caractère | nom:sub(1, 1) |
attempt to index a number value | x.champ ou x:methode() sur un nombre | la variable ne contient pas ce que vous croyez : affichez-la |
table index is nil | t[cle] = valeur alors que cle vaut nil | vérifier la clé |
'for' limit must be a number | for i = 1, nombre alors que nombre vaut nil ou du texte | tonumber, ou une valeur par défaut |
attempt to iterate over a nil value | for v in liste alors que liste vaut nil | vérifier la liste |
use pairs() or ipairs() to iterate a table with two variables | for k, v in t | for k, v in pairs(t) |
an iterator can only be used in a 'for' loop | appeler pairs(t) hors d'un for | l'utiliser dans un for |
string too long | un texte de plus de 65536 caractères | le couper, ou l'écrire en plusieurs fichiers |
no storage medium | fs sans support dans l'ordinateur | insérer un support |
not enough space | le support est plein | effacer des fichiers, ou prendre un support plus grand |
Les limites : pile, mémoire et minuteurs
Trois erreurs viennent des limites qui gardent chaque ordinateur petit et équitable. Voir Limites pour toutes.
stack overflow (débordement de pile) : plus de 200 appels de fonction imbriqués, presque toujours une fonction récursive qui ne s'arrête jamais.
local function profondeur(n)
return profondeur(n + 1) -- aucune condition d'arrêt
end
profondeur(1)snippet:2: stack overflow
Donnez à la fonction un cas où elle ne s'appelle pas elle-même, ou réécrivez-la avec une boucle.
out of memory (mémoire épuisée) : le programme garde plus de données que la mémoire de l'ordinateur n'en contient (de 2 K cellules sur le Calculateur à tubes à 1 M sur l'Ordinateur moderne). Le coupable habituel est une table qui grandit sans fin, comme un historique de mesures :
local historique = {}
local n = 0
while true do
n = n + 1
historique[n] = "mesure " .. n -- rien n'est jamais retiré
endsnippet:5: out of memory
Ne gardez que le nécessaire (if #historique > 100 then table.remove(historique, 1) end), écrivez les vieilles données dans un fichier, et surveillez os.memory().used. La commande mem du shell affiche la mémoire utilisée. Vitesse, mémoire et limites explique ce qui prend de la mémoire.
too many timers (trop de minuteurs) : plus de 256 minuteurs attendent en même temps, en général parce que os.start_timer est appelé dans une boucle qui ne les attend pas.
for i = 1, 300 do
os.start_timer(60)
endsnippet:2: too many timers
Ne lancez un nouveau minuteur que quand le précédent s'est déclenché (voir Temps et minuteries).
Les erreurs de compilation courantes
Une erreur de compilation arrête le programme avant sa première ligne. Les plus fréquentes :
| message | cause habituelle |
|---|---|
'end' expected (to close 'if' at line 2) near <eof> | un bloc (if, for, while, function, do) sans son end |
'<eof>' expected near 'end' | un end de trop |
'then' expected near '=' | if x = 3 then : comparer s'écrit == |
'do' expected near 'print' | while ou for sans do |
unexpected symbol near '=' | il manque une valeur : local x = = 3, print(1 +) |
syntax error near '+' | un calcul seul sur une ligne : compte + 1 au lieu de compte = compte + 1 |
unfinished string | il manque le guillemet de fin |
'}' expected (to close '{' at line 1) near 'contrainte' | il manque une virgule entre deux champs d'une table |
')' expected (to close '(' at line 1) near <eof> | il manque une parenthèse fermante |
malformed number near '3x' | une lettre collée à un nombre |
invalid escape sequence '\q' | une barre oblique inverse dans une chaîne : écrivez \\ |
cannot assign to this expression | f() = 3 : seuls les variables et les champs reçoivent une valeur |
'break' outside a loop | un break qui n'est dans aucun for, while ou repeat |
ambiguous syntax (function call x new statement) near '(' | une ligne qui commence par ( : mettez ; au début, ou collez-la à la ligne du dessus |
Et les règles où Brass diffère de Lua, expliquées dans Brass pour les habitués de Lua et ComputerCraft :
| message | que faire |
|---|---|
cannot capture local 'compte' of an enclosing function | sortir compte au niveau du fichier, ou le passer en paramètre |
a function returns a single value (return a table instead) | return {x = x, y = y} au lieu de return x, y |
variable arguments ('...') are not supported | prendre une table en paramètre |
unexpected symbol '!' (use 'not') | not x ; != est accepté, ! seul ne l'est pas |
Une table où deux champs n'ont pas de virgule entre eux :
local presse = {
vitesse = 64
contrainte = 2
}snippet:3: '}' expected (to close '{' at line 1) ne
ar 'contrainte'Le compilateur attendait la fin de la table après vitesse = 64 et a trouvé le mot contrainte à la place : la virgule va à la fin de la ligne 2.
Lever vos propres erreurs
error
error(message) arrête le programme avec votre message, positionné comme toute autre erreur. Servez-vous-en quand le programme ne peut pas continuer de façon sensée : un appareil absent, un réglage hors limites. Un message clair fait gagner du temps plus tard.
local function regler_vitesse(tpm)
if tpm < -256 or tpm > 256 then
error("vitesse hors limites : " .. tpm)
end
print("vitesse " .. tpm)
end
regler_vitesse(128)
regler_vitesse(300)vitesse 128 snippet:3: vitesse hors limites : 300
La position est toujours la ligne de l'appel à error (le second argument de Lua, le niveau, est ignoré). Donnez-lui une chaîne : les autres valeurs sont changées en texte, et une table devient quelque chose comme table: 0x1b6d3586.
assert
assert(valeur, message) vérifie une valeur : si elle vaut nil ou false, il s'arrête avec le message (ou assertion failed! sans message). Sinon il renvoie la valeur, ce qui donne une façon compacte de lire quelque chose qui doit exister :
fs.write("trajet", "dépôt,mine,ferme")
local trajet = assert(fs.read("trajet"), "pas de fichier trajet")
print(#trajet:split(",") .. " arrêts")
local horaires = assert(fs.read("horaires"), "pas de fichier horaires")3 arrêts snippet:4: pas de fichier horaires
Rattraper les erreurs avec pcall
pcall(f, arguments...) appelle f avec ces arguments et rattrape toute erreur qu'elle lève. Au lieu d'arrêter le programme, il renvoie une table :
{ok = true, value = ...}quandfa réussi (valueest ce quefa renvoyé) ;{ok = false, error = "..."}quand elle a échoué (errorest le message, position comprise).
local function lire_niveau(texte)
local n = tonumber(texte)
if n == nil then
error("pas un nombre : " .. texte)
end
return n
end
local r = pcall(lire_niveau, "12")
print(r.ok, r.value)
r = pcall(lire_niveau, "douze")
print(r.ok, r.error)true 12 false snippet:4: pas un nombre : douze
Il rattrape aussi les erreurs des fonctions de bibliothèque : c'est ainsi qu'un programme survit à un bloc cassé par un joueur (traffic light removed), à une mauvaise valeur d'un capteur ou à un disque plein.
Passez les arguments à pcall après la fonction, comme ci-dessus. En Lua on écrirait souvent pcall(function() return coffre.push("right", 1, nombre) end), mais en Brass une fonction écrite dans une autre ne peut pas utiliser les variables locales de celle-ci (cannot capture local). pcall(coffre.push, "right", 1, nombre) fait la même chose et compile toujours.
Si le premier argument n'est pas une fonction, pcall n'échoue pas : il renvoie {ok = false, error = ...}, par exemple {ok = false, error = "attempt to call a nil value"} pour un nom de fonction mal écrit.
Réessayer
Un appareil occupé, une machine mobile pas encore assemblée : parfois, réessayer un peu plus tard est la bonne réponse.
local essais = 0
local function lire_deployeur()
essais = essais + 1
if essais < 3 then
error("déployeur occupé")
end
return 42
end
local function reessayer(f, fois)
for i = 1, fois do
local r = pcall(f)
if r.ok then
return r
end
print("essai " .. i .. " : " .. r.error)
sleep(0.5)
end
return {ok = false, error = "abandon après " .. fois .. " essais"}
end
local r = reessayer(lire_deployeur, 5)
print(r.ok, r.value)essai 1 : snippet:5: déployeur occupé essai 2 : snippet:5: déployeur occupé true 42
Garder un programme en vie
Un programme de contrôle qui tourne des jours ne doit pas mourir à la première surprise. Faites le vrai travail dans une fonction, rattrapez ce qui tourne mal, notez-le dans un fichier, mettez les machines dans un état sûr, et recommencez :
local function principal()
-- tout le programme : lire les capteurs, piloter les sorties...
end
while true do
local r = pcall(principal)
if r.ok then
break -- principal s'est terminé normalement : le travail est fini
end
rs.set("back", 0) -- arrêter la presse
fs.append("plantages.log", r.error .. "\n")
print("redémarrage après : " .. r.error)
sleep(5)
endRanger, puis s'arrêter
Pour s'arrêter après un échec mais mettre d'abord les sorties dans un état sûr, rattrapez l'erreur, rangez, puis relevez-la :
local function faire_tourner_presse()
error("presse bloquée")
end
local r = pcall(faire_tourner_presse)
print("presse coupée")
if not r.ok then
error(r.error)
endpresse coupée snippet:8: snippet:2: presse bloquée
Le message reçoit une seconde position, car error ajoute la ligne où il est appelé. Les deux sont utiles : la ligne 8 est celle où le programme s'est arrêté, la ligne 2 celle où le problème a commencé. Pour ne garder que le message d'origine, affichez-le en rouge (term.set_fg(term.colors.red)) et terminez le programme avec return.
Ce que pcall ne rattrape pas
- Ctrl+T (le bouton Stopper du terminal) termine le programme sur-le-champ. Ce n'est pas une erreur et aucun code à vous ne s'exécute après : l'écran est effacé et affiche
Terminated. Les sorties restent comme elles étaient. os.rebootetos.shutdown(et Ctrl+R) terminent aussi le programme.- Un ordinateur sans rotation n'est pas en erreur : il est figé, et reprend là où il en était quand l'arbre tourne à nouveau.
Techniques de débogage
Certains bugs ne lèvent aucune erreur : le programme tourne, mais fait la mauvaise chose. Il faut alors regarder dedans.
Afficher ce qui se passe
L'outil le plus simple est le meilleur : affichez avec print les valeurs juste avant la ligne qui se comporte mal, avec une étiquette et avec leur type.
local mesures = {"12", "15", "9"} -- telles que lues dans un fichier
local total = 0
for i, m in ipairs(mesures) do
print("mesure", i, m, type(m))
total = total + tonumber(m)
end
print("total", total)mesure 1 12 string mesure 2 15 string mesure 3 9 string total 36
type(valeur) répond "nil", "boolean", "number", "string", "table" ou "function". La moitié des bugs sont une valeur du mauvais type : un nombre qui est du texte, une table qui vaut nil.
Une table s'affiche table: 0x1b6d3586, ce qui ne dit rien de son contenu. Une petite fonction le montre, tables dans les tables comprises :
local function detailler(t, retrait)
retrait = retrait or ""
for k, v in pairs(t) do
if type(v) == "table" then
print(retrait .. tostring(k) .. " :")
detailler(v, retrait .. " ")
else
print(retrait .. tostring(k) .. " = " .. tostring(v))
end
end
end
detailler({gare = "Mine de fer", stock = {fer = 120, cuivre = 8}, ouverte = true})gare = Mine de fer stock : fer = 120 cuivre = 8 ouverte = true
Allumez et éteignez les traces avec un interrupteur en haut du fichier, plutôt que de les effacer :
local DEBUG = true
local function trace(message)
if DEBUG then
print("[debug] " .. message)
end
end
trace("le coffre renforcé a " .. 1250 .. " fer")[debug] le coffre renforcé a 1250 fer
Un fichier journal
Quand l'écran est trop petit, ou que le bug arrive la nuit sans personne pour regarder, écrivez les traces dans un fichier avec fs.append. Chaque appel ajoute du texte à la fin du fichier (et le crée la première fois) :
local JOURNAL = "debug.log"
fs.delete(JOURNAL) -- un journal neuf à chaque lancement
local function noter(message)
fs.append(JOURNAL, message .. "\n")
end
noter("démarrage")
noter("coffre renforcé : 1250 fer")
noter("presse lancée")
write(fs.read(JOURNAL))démarrage coffre renforcé : 1250 fer presse lancée
Relisez-le plus tard avec cat debug.log à l'invite, ou edit debug.log. Ajoutez l'heure avec string.format("[%7.1f] %s\n", os.clock(), message) : os.clock() donne les secondes depuis le démarrage de l'ordinateur.
Un journal grandit à chaque ligne : un Paquet de cartes perforées contient 4 Ko, une Disquette 64 Ko, et un support plein arrête le programme avec not enough space. Effacez le journal au démarrage, comme ci-dessus, ou gardez-le court.
L'invite brass
Tapez brass à l'invite pour ouvrir l'interpréteur interactif : chaque ligne tapée s'exécute aussitôt, et sa valeur s'affiche. C'est le moyen le plus rapide d'essayer une expression, de vérifier ce que renvoie une fonction ou de lire un capteur à la main.
> brass Brass 1.0 - type 'exit' to leave. brass> ("minecraft:iron_ingot"):find(":") 10 brass> math.floor(1250 / 64) 19 brass> tonumber("12 objets") brass> niveau = rs.get("left") brass> niveau 0 brass> exit >
Une ligne dont la valeur est nil n'affiche rien (tonumber("12 objets") ci-dessus). Chaque ligne est un petit programme à part, donc une variable local est oubliée à la ligne suivante : utilisez des variables globales (niveau = ...) pour garder une valeur d'une ligne à l'autre. Les erreurs s'affichent brass:1: .... Ctrl+T ou exit ramène au shell.
Resserrer l'enquête
- Placez
print("ici 1"),print("ici 2")... entre les étapes : la dernière affichée dit où ça déraille. - Mettez une partie du programme en commentaire avec
--[[et]]pour voir si le bug disparaît. - Ralentissez le programme pour le regarder faire : un
sleep(1)entre les étapes, ouos.pull_event("key")pour avancer d'une étape par touche. Un arbre plus lent ralentit aussi l'ordinateur (voirLes ordinateurs). - Vérifiez vos hypothèses en haut d'une fonction, pour qu'une mauvaise valeur échoue tôt, près de sa cause :
local function regler_lampe(cote, niveau)
assert(type(cote) == "string", "le côté doit être du texte, reçu " .. type(cote))
assert(niveau >= 0 and niveau <= 15, "le niveau doit aller de 0 à 15, reçu " .. niveau)
rs.set(cote, niveau)
endQuand quelque chose ne va pas : la liste
- Lisez le fichier et la ligne, et ouvrez-les avec
edit. - Lisez le nom entre parenthèses : c'est la valeur qui n'était pas celle que vous attendiez.
- Affichez avec
printles valeurs utilisées sur cette ligne, avec leurtype. - Essayez l'expression à l'invite
brass. - Regardez de quoi la ligne dépend dans le monde : un bloc déplacé, un coffre vide, un disque plein, un ordinateur qui n'a pas la bonne bibliothèque.
Voir aussi
Messages d'erreur: tous les messages d'erreur, avec leur cause.pcall(),error(),assert(): la référence des trois fonctions.Brass pour les habitués de Lua et ComputerCraft: les erreurs de compilation qui viennent des habitudes de Lua.Limites: mémoire, profondeur d'appel, minuteurs, longueur des chaînes.