Create: Computing AgesDoc Brass
Le langage Brass

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 :

Terminal
> 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 :

  1. Le fichier : porte. Un fichier rangé dans un dossier montre son chemin complet (apps/porte/main:12:), une ligne tapée à l'invite brass s'appelle brass, et sur ce site les exemples s'appellent snippet. Quand un programme est découpé en plusieurs fichiers avec import(), c'est ce qui vous dit lequel ouvrir.
  2. La ligne : 12. Tapez edit porte : l'éditeur affiche les numéros de ligne dans la marge.
  3. 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 locale lampe valait nil.

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.

Attention

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.

Brass
local niveau = 12
if niveau > 10 then
  print("trop haut")
Écran
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.

Brass
local texte = fs.read("reglages")   -- nil : ce fichier n'existe pas
print(texte:upper())
Écran
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 :

Brass
local reglages = {gare = "Mine de fer"}
print(reglages.alarme.cote)
Écran
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 nil quand elle ne trouve rien : fs.read (pas de fichier), peripheral.wrap et peripheral.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.Gare n'est pas reglages.gare) ;
  • une bibliothèque que cet ordinateur n'a pas : gfx sur le Calculateur à tubes, net avant le Mini-ordinateur. Le message est alors attempt to index a nil value (global 'net').

Correction : testez avant d'utiliser, et dites ce qui manque.

Brass
local lampe = peripheral.wrap("top")
if lampe == nil then
  error("pas de lampe sur le dessus de l'ordinateur")
end

attempt 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.

Brass
term.setCursorPos(1, 1)
Écran
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 :

Brass
saluer("Steve")
function saluer(nom)
  print("Bonjour, " .. nom)
end
Écran
snippet: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.

Brass
local stock = {fer = 120}
print(stock.fer + stock.cuivre)
Écran
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.

Brass
local arrets = {"Mine de fer", "Usine de laiton"}
print("Prochain arrêt : " .. arrets[3])
Écran
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 :

Brass
local niveau = "12"   -- lu dans un fichier : c'est du texte
if niveau > 10 then
  print("trop haut")
end
Écran
snippet: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) :

Brass
local tape = "3.7"
print(math.floor(tape))
Écran
snippet:2: bad argument #1 to 'floor' (number expec
ted, got string)

Un argument manquant, ou nil, s'affiche got no value :

Brass
print(string.rep("-"))
Écran
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

messagecausecorrection
attempt to get length of a nil value#x alors que x vaut nilvérifier que la table existe
attempt to index a string value with a number keynom[1] pour avoir un caractèrenom:sub(1, 1)
attempt to index a number valuex.champ ou x:methode() sur un nombrela variable ne contient pas ce que vous croyez : affichez-la
table index is nilt[cle] = valeur alors que cle vaut nilvérifier la clé
'for' limit must be a numberfor i = 1, nombre alors que nombre vaut nil ou du textetonumber, ou une valeur par défaut
attempt to iterate over a nil valuefor v in liste alors que liste vaut nilvérifier la liste
use pairs() or ipairs() to iterate a table with two variablesfor k, v in tfor k, v in pairs(t)
an iterator can only be used in a 'for' loopappeler pairs(t) hors d'un forl'utiliser dans un for
string too longun texte de plus de 65536 caractèresle couper, ou l'écrire en plusieurs fichiers
no storage mediumfs sans support dans l'ordinateurinsérer un support
not enough spacele support est pleineffacer 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.

Brass
local function profondeur(n)
  return profondeur(n + 1)  -- aucune condition d'arrêt
end
profondeur(1)
Écran
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 :

Brass
local historique = {}
local n = 0
while true do
  n = n + 1
  historique[n] = "mesure " .. n   -- rien n'est jamais retiré
end
Écran
snippet: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.

Brass
for i = 1, 300 do
  os.start_timer(60)
end
Écran
snippet: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 :

messagecause 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 stringil 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 expressionf() = 3 : seuls les variables et les champs reçoivent une valeur
'break' outside a loopun 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 :

messageque faire
cannot capture local 'compte' of an enclosing functionsortir 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 supportedprendre 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 :

Brass
local presse = {
  vitesse = 64
  contrainte = 2
}
Écran
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.

Brass
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)
Écran
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 :

Brass
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")
Écran
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 = ...} quand f a réussi (value est ce que f a renvoyé) ;
  • {ok = false, error = "..."} quand elle a échoué (error est le message, position comprise).
Brass
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)
Écran
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.

Astuce

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.

Brass
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)
Écran
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 :

Brass
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)
end

Ranger, 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 :

Brass
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)
end
Écran
presse 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.reboot et os.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.

Brass
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)
Écran
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 :

Brass
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})
Écran
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 :

Brass
local DEBUG = true

local function trace(message)
  if DEBUG then
    print("[debug] " .. message)
  end
end

trace("le coffre renforcé a " .. 1250 .. " fer")
Écran
[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) :

Brass
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))
Écran
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.

Terminal
> 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, ou os.pull_event("key") pour avancer d'une étape par touche. Un arbre plus lent ralentit aussi l'ordinateur (voir Les ordinateurs).
  • Vérifiez vos hypothèses en haut d'une fonction, pour qu'une mauvaise valeur échoue tôt, près de sa cause :
Brass
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)
end

Quand quelque chose ne va pas : la liste

  1. Lisez le fichier et la ligne, et ouvrez-les avec edit.
  2. Lisez le nom entre parenthèses : c'est la valeur qui n'était pas celle que vous attendiez.
  3. Affichez avec print les valeurs utilisées sur cette ligne, avec leur type.
  4. Essayez l'expression à l'invite brass.
  5. 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