Create: Computing AgesDoc Brass
Le langage Brass

Brass pour les habitués de Lua et ComputerCraft

Toutes les différences avec Lua 5.x et avec les API de ComputerCraft, leur raison d'être, et ce qu'il faut écrire à la place.

Si vous connaissez Lua, ou avez programmé des ordinateurs ComputerCraft, vous connaissez déjà l'essentiel de Brass : les mêmes mots-clés, les mêmes tables, local, pairs, .., string.format. Cette page liste tout ce qui diffère, pour que vos habitudes travaillent pour vous et non contre vous. Un tableau comparatif clôt la page.

La plupart des différences ont l'une de deux raisons. Brass tourne sur sa propre petite machine virtuelle à l'intérieur du serveur, qui compte chaque instruction et chaque cellule de mémoire de chaque ordinateur : aucun programme ne peut faire laguer le serveur ni dévorer sa mémoire. Et Brass est fait pour s'apprendre en jeu : quelques pièges de Lua ont été retirés ou changés en erreurs claires.

Ce qui ne change pas

  • La syntaxe : local, function, if ... elseif ... else ... end, while, repeat ... until, le for numérique, for k, v in pairs(t), break, do ... end, return.
  • Les commentaires -- et --[[ ... ]], les chaînes entre "...", '...' et [[ ... ]].
  • Les tables indexées à partir de 1, #t, t.nom et t[cle], {1, 2, x = 3}.
  • and, or, not, avec la même règle « seuls nil et false sont faux » (donc 0 et "" sont vrais).
  • // (division entière) et % comme en Lua 5.3, ^ pour les puissances.
  • Les appels de méthode obj:methode(x) et function obj:methode(x) avec self, les raccourcis d'appel f "texte" et f {table}.
  • tostring, tonumber, type, pairs, ipairs, error, assert, et les fonctions habituelles de string, table et math.

Le langage

Pas de fermetures sur les locales d'une fonction

C'est la différence que vous rencontrerez en premier. Une fonction écrite à l'intérieur d'une autre fonction ne peut pas utiliser les variables locales ni les paramètres de celle-ci. Le grand classique de Lua ne compile pas :

Brass
local function creer_compteur()
  local n = 0
  return function()
    n = n + 1
    return n
  end
end
Écran
snippet:4: cannot capture local 'n' of an enclosing
 function

Ce qu'une fonction peut utiliser :

  • ses propres paramètres et locales ;
  • les variables globales ;
  • les locales du niveau du fichier (déclarées hors de toute fonction) : elles se comportent comme des variables du module, partagées par toutes les fonctions du fichier ;
  • elle-même, quand elle est déclarée avec local function f (la récursivité marche).

L'état va donc dans une locale du niveau du fichier, dans une table passée en paramètre, ou dans une table utilisée comme objet :

Brass
local function nouveau_compteur()
  return {n = 0}
end

local function avancer(compteur)
  compteur.n = compteur.n + 1
  return compteur.n
end

local c = nouveau_compteur()
avancer(c)
print(avancer(c))
Écran
2

La variable d'une boucle for écrite au niveau du fichier est une seule variable du fichier, pas une copie neuve à chaque tour : une fonction créée dans la boucle voit sa valeur du moment où elle est appelée, pas celle de son tour.

Brass
local actions = {}
for i = 1, 3 do
  actions[i] = function() return i end
end
print(actions[1](), actions[3]())
Écran
3   3

Rangez la valeur dans une table (actions[i] = {etape = i}) quand chacune a besoin de la sienne.

Une fonction renvoie une seule valeur

return a, b est une erreur de compilation : a function returns a single value (return a table instead). Renvoyez une table :

Brass
local function en_piles(nombre)
  return {piles = nombre // 64, reste = nombre % 64}
end
local p = en_piles(1234)
print(p.piles, p.reste)
Écran
19  18

local a, b = f() compile, mais b vaut toujours nil. L'affectation multiple elle-même marche : a, b = b, a échange deux valeurs.

La bibliothèque suit la même règle. Là où Lua renvoie plusieurs valeurs, Brass en renvoie une, ou une table :

LuaBrass
local i, j = s:find("x")local i = s:find("x"), la fin est i + #"x" - 1
local ok, err = pcall(f)local r = pcall(f), puis r.ok, r.value, r.error
local x, y = term.getCursorPos()local c = term.get_cursor(), puis c.x, c.y
local w, h = term.getSize()local t = term.get_size(), puis t.w, t.h
local event, a, b = os.pullEvent()local e = os.pull_event(), puis e.name et des champs nommés

Pas de ..., ni select, ni unpack

function f(...) est une erreur de compilation (variable arguments ('...') are not supported), et select, unpack et table.unpack n'existent pas. Donnez à une fonction une table au lieu d'un nombre variable d'arguments : somme({3, 4, 5}). Les fonctions de bibliothèque acceptent toujours plusieurs arguments quand c'est utile : print, write, math.min, math.max, string.format, string.char.

Les arguments d'un programme tapés à l'invite (run stock nord 64, ou stock nord 64) sont dans la table globale arg : arg[1] vaut "nord", arg[2] vaut "64" (du texte), et arg[0] est le chemin du programme.

Pas de métatables

setmetatable, getmetatable, rawget, rawset, rawequal, rawlen et tous les __index, __call, __add... sont absents. Un objet est une simple table qui contient ses données et ses fonctions ; obj:methode() passe la table en self. Pour donner les mêmes fonctions à plusieurs objets, copiez-les dedans, avec table.copy par exemple :

Brass
local Machine = {}
function Machine.decrire(self)
  return self.nom .. " à " .. self.vitesse .. " tr/min"
end

local function nouvelle_machine(nom, vitesse)
  local m = table.copy(Machine)
  m.nom = nom
  m.vitesse = vitesse
  return m
end

local presse = nouvelle_machine("Presse mécanique", 64)
print(presse:decrire())
Écran
Presse mécanique à 64 tr/min

Programmes en plusieurs fichiers montre la même idée sous forme d'un module qui construit des objets.

Pas de coroutines

Il n'y a pas de bibliothèque coroutine, ni de parallel de ComputerCraft. Un programme Brass suit un seul fil d'exécution. Pour faire plusieurs choses à la fois (faire clignoter une lampe, surveiller un levier, répondre au clavier), écrivez une seule boucle d'évènements qui traite chaque sorte d'évènement, avec des minuteurs pour les tâches périodiques :

Brass
local clignotement = os.start_timer(0.5)
local allumee = false

while true do
  local e = os.pull_event()
  if e.name == "timer" and e.id == clignotement then
    allumee = not allumee
    rs.set("top", allumee)
    clignotement = os.start_timer(0.5)
  elseif e.name == "redstone" then
    print("levier : " .. rs.get("left"))
  elseif e.name == "key" and e.key == "enter" then
    print("Entrée pressée")
  end
end

Évènements construit ce modèle pas à pas.

Pas de goto

goto et les étiquettes (::continue::) n'existent pas : goto continue s'arrête sur syntax error near 'continue'. Pour sauter la fin d'un tour, mettez-la dans un if :

Brass
for _, objet in ipairs({"fer", "", "or"}) do
  if objet ~= "" then
    print(objet)
  end
end
Écran
fer
or

Les chaînes

  • Pas de motifs. string.find cherche le texte exactement tel qu'il est écrit (son quatrième argument est ignoré) et ne renvoie que la position de début. string.match, string.gmatch et string.gsub n'existent pas. Travailler avec du texte montre comment faire chaque tâche courante sans eux.
  • string.format connaît %d %i %f %s %x %X %%, avec les drapeaux - 0 + espace, une largeur et une précision d'au plus deux chiffres. %q, %e, %g, %c, %o et %a sont refusés.
  • Des caractères, pas des octets. #"café" vaut 4 (5 en Lua), string.byte donne des codes de caractère jusqu'à 65535 ("€" vaut 8364), string.char les accepte, et upper/lower gèrent les accents ("été" donne "ÉTÉ").
  • Pas de conversion automatique dans les calculs : "10" + 1 est une erreur, pas 11. tonumber convertit. .. change toujours les nombres en texte.
  • Séquences d'échappement : seulement \n \t \r \\ \" \' \0 \xNN (pas de \ddd, \u{...}, \z, \a...). Les chaînes longues n'ont qu'un niveau, [[ ... ]] (pas de [==[).
  • Extensions : string.split, string.trim, string.starts, string.ends.
  • Une chaîne contient au plus 65536 caractères.

Les nombres

  • Une seule sorte de nombre, un flottant sur 64 bits. Il n'y a pas de sous-type entier : 10 / 2 affiche 5 (Lua 5.3 affiche 5.0), et math.type, math.tointeger, math.maxinteger et math.modf n'existent pas. Les nombres entiers sont exacts jusqu'à 2^53.
  • Les nombres s'affichent comme le %.14g de Lua : 0.1 + 0.2 affiche 0.3.
  • Pas d'opérateurs bit à bit : &, |, ~, <<, >> sont des erreurs de compilation (unexpected symbol '&'), et il n'y a pas de bit32. Passez par l'arithmétique : le bit n de x vaut x // 2 ^ n % 2.
  • Extension : math.round.

Les tables et les boucles

  • for v in liste do parcourt les valeurs d'une liste (une extension de Brass). for k, v in t sans pairs est une erreur qui vous le dit.
  • pairs parcourt la partie liste dans l'ordre, puis les autres clés dans l'ordre où elles ont été ajoutées : le même ordre à chaque lancement, contrairement à Lua. Ajouter ou retirer des clés pendant la boucle ne pose pas de problème.
  • table.sort est stable (les éléments égaux gardent leur ordre). table.remove(t) sur une table vide renvoie nil (table.remove(t, 1) est une erreur).
  • Extensions : table.copy (une copie de surface), table.contains, table.keys.
  • Absents : table.unpack, table.pack, table.move, et next. Pour savoir si une table est vide, utilisez #table.keys(t) == 0.
  • != marche comme ~=.

Les erreurs

  • pcall renvoie une table : {ok = true, value = ...} ou {ok = false, error = "..."}. Méfiez-vous de l'habitude de Lua, qui compile mais ne fait pas ce que vous croyez :
Brass
local ok, err = pcall(error, "boum")
print(type(ok), err)
local r = pcall(error, "boum")
print(r.ok, r.error)
Écran
table   nil
false   snippet:3: boum
  • error(message) ajoute toujours la position fichier:ligne: de l'appel ; son second argument (le niveau) est ignoré. Une valeur qui n'est pas une chaîne devient du texte : on ne peut donc pas lever une table pour transporter des données.
  • Pas de xpcall ni de trace d'appels : un message nomme la ligne où l'erreur s'est produite, pas les appels qui y ont mené.
  • Ctrl+T ne se rattrape pas : il n'y a pas d'évènement terminate ni d'os.pullEventRaw.

Erreurs et débogage traite tout cela en détail.

Les globales absentes

load, loadstring, dofile, loadfile, next, select, unpack, rawget et compagnie, setmetatable, getmetatable, collectgarbage, xpcall, _G, _ENV et _VERSION n'existent pas, pas plus que les bibliothèques coroutine, io, debug, utf8, package et bit32. Dans os, seules les fonctions de os existent (pas d'os.date, os.exit, os.getenv, os.remove...). Appeler une fonction absente s'arrête sur attempt to call a nil value (global 'load'). Pour exécuter un autre fichier, utilisez import().

Les API, comparées à ComputerCraft

Des noms en snake_case

Tous les noms de Brass sont en minuscules avec des tirets bas : os.pull_event, os.start_timer, os.queue_event, os.day_time, term.set_cursor, term.get_size, fs.make_dir, fs.is_dir. Un nom de ComputerCraft s'arrête sur attempt to call a nil value (field 'pullEvent').

Les évènements sont des tables

os.pull_event([nom]) renvoie une table, avec le nom dans e.name et les données dans des champs nommés :

Brass
-- ComputerCraft : local event, key = os.pullEvent("key")
local e = os.pull_event("key")
if e.key == "enter" then
  print("confirmé")
end
évènementchamps
timerid
keykey : un nom ("enter", "backspace", "delete", "up", "down", "left", "right", "home", "end", "tab")
charchar : le caractère tapé (lettres, chiffres et espace arrivent ici, pas en key)
pastetext
click, dragx, y (caractère), px, py (pixel), button (1, 2, 3), source ("terminal" ou "monitor")
redstoneaucun : lisez les côtés avec rs.get
messagesender, channel, data, via, distance
diskinserted (true ou false)

Les touches sont des noms, pas des nombres : il n'y a pas de table keys, ni d'indicateur « touche maintenue ». os.queue_event(nom, donnee) transporte une seule valeur, dans e.data. Comme dans ComputerCraft, attendre un nom précis jette les autres évènements, et la file contient au plus 256 évènements. La liste complète est dans Évènements.

La redstone : rs

Il n'y a pas de table redstone, seulement rs, et elle parle en niveaux :

ComputerCraftBrass
rs.setOutput("top", true)rs.set("top", true) ou rs.set("top", 15)
rs.setAnalogOutput("top", 7)rs.set("top", 7)
rs.getInput("left")rs.get("left") > 0
rs.getAnalogInput("left")rs.get("left")
rs.getSides()rs.sides()

rs.get renvoie un nombre, et 0 est vrai dans une condition : if rs.get("left") then est toujours pris. Comparez avec > 0. Les côtés sont front, back, left, right, top, bottom, gauche et droite vus en faisant face à l'écran. Voir rs.

Les périphériques

peripheral.wrap(nom) renvoie une table de fonctions (ou nil quand il n'y a pas d'appareil). Appelez-les avec un point, comme les périphériques de ComputerCraft :

Brass
local feu = peripheral.wrap("left")
feu.set("green")

Les deux-points (feu:set("green")) passeraient la table elle-même en premier argument, et l'appel échouerait sur bad argument #1 to 'set' (string expected, got table). (Les deux-points sont pour vos propres objets, voir plus haut.)

ComputerCraftBrass
peripheral.getNames()peripheral.list(), ou peripheral.list("inventory") pour un seul type
peripheral.getType(nom)peripheral.type(nom)
peripheral.find(type) : toutes les correspondancesperipheral.find(type) : la première seulement ; enveloppez les noms de peripheral.list(type) pour toutes
peripheral.call(nom, methode, ...)pareil
peripheral.getMethods(nom)la table enveloppée est une simple table : table.keys(appareil)
des noms réseau comme "monitor_0"les faces ("left") ou "type@x,y,z" pour un bloc sur un Câble de données

Les appareils et leurs méthodes sont propres à Brass (machines de Create, coffres à objet renforcés, Feux tricolores, capteurs) : voir Tous les périphériques. On n'écrit pas sur un moniteur par peripheral : un moniteur qui touche l'ordinateur affiche son écran, donc print, term et gfx dessinent directement dessus (l'envelopper donne seulement sa taille, @monitor.size).

Le terminal et les couleurs

  • term.setTextColor et term.setBackgroundColor sont term.set_fg et term.set_bg ; term.setCursorPos est term.set_cursor ; term.get_cursor et term.get_size (les getCursorPos et getSize de ComputerCraft) renvoient des tables, {x = ..., y = ...} et {w = ..., h = ...}.
  • Les couleurs sont des nombres de 0 à 15, pas les puissances de deux de ComputerCraft, et elles vivent dans term.colors (il n'y a pas de colors ni de colours global) :
0123456789101112131415
whiteorangemagentalight_blueyellowlimepinkgraylight_graycyanpurplebluebrowngreenredblack

Une valeur de ComputerCraft comme colors.red (16384) s'arrête sur bad argument #1 to 'set_fg' (color must be 0..15). Le nombre de Brass est l'exposant de la puissance de deux de ComputerCraft : 16384 vaut 2^14, donc le rouge est 14. Les fonctions de gfx acceptent aussi les noms en texte : gfx.rect(1, 1, 20, 10, "red").

  • Il n'y a ni term.blit, ni term.redirect, ni term.isColor, ni window, ni paintutils. Le dessin passe par gfx (pixels, lignes, formes, petit texte). Les écrans des premiers âges (papier, phosphore vert et ambre) dessinent tout dans leur unique encre.

Les fichiers

Il n'y a ni descripteurs de fichier (handles) ni io. Un fichier se lit et s'écrit en un seul appel :

ComputerCraftBrass
fs.open(p, "r") puis h.readAll(), h.close()fs.read(p) : tout le texte, ou nil
lire ligne à ligne avec h.readLine()fs.read(p):split("\n")
fs.open(p, "w") puis h.write(t), h.close()fs.write(p, t) (crée les dossiers en chemin)
fs.open(p, "a")fs.append(p, t)
fs.makeDir, fs.isDir, fs.getSizefs.make_dir, fs.is_dir, fs.size
fs.getFreeSpace, fs.getCapacityfs.free(), fs.capacity()
fs.combine(a, b)a .. "/" .. b

Les chemins partent du dossier courant (fs.cwd()), ou de la racine avec un / au début. Les noms utilisent des lettres, des chiffres, _, . et -. Voir fs.

Le réseau

net remplace rednet, sans rednet.open : le Câble de données, le Modem radio ou le Routeur Wi-Fi se charge de la connexion.

ComputerCraftBrass
rednet.open("back")rien à faire
rednet.send(id, msg, protocole)net.send(id, donnee, canal) : true si livré
rednet.broadcast(msg, protocole)net.broadcast(donnee, canal) : le nombre d'ordinateurs atteints
local id, msg = rednet.receive(protocole, 5)local m = net.receive(5) : m.sender, m.data, m.channel, ou nil après 5 s
os.getComputerID()os.id() ou net.id()

net.receive ne prend pas de canal : testez m.channel vous-même. Les messages sont des copies de données simples (nombres, texte, booléens, tables de ces valeurs), jusqu'à 32768 caractères. net existe à partir du Mini-ordinateur. Voir net et Réseaux.

Les programmes et le shell

  • L'interpréteur interactif est brass, pas lua.
  • Le fichier de démarrage s'appelle exactement startup (pas startup.lua, pas de dossier startup).
  • Les arguments d'un programme arrivent dans la globale arg, pas dans ....
  • Il n'y a pas d'API shell (pas de shell.run) ni de multishell : un seul programme tourne à la fois. Un programme en lance un autre avec import, qui charge aussi les bibliothèques avec des chemins relatifs au fichier (import "lib/texte"), voir Programmes en plusieurs fichiers. require est la même fonction.
  • Pas de textutils (net envoie les tables telles quelles ; pour ranger une table dans un fichier, écrivez ses champs en texte), pas de settings (utilisez un fichier de réglages, voir Travailler avec du texte).
  • os.getComputerLabel et os.setComputerLabel sont os.label() et os.label(texte).
  • read prend un seul argument facultatif, un caractère de masque pour les mots de passe : read("*"). Pas d'historique ni de complétion en arguments.
  • Pas de http, disk, gps, turtle ni pocket. À leur place, des bibliothèques pour Create : link (les Liaisons de redstone), display (les Liaisons d'affichage), vehicle (Create Aeronautics).

Le temps

  • sleep(secondes) attend ceil(secondes × 20) ticks, au moins un : sleep(0) attend un tick. Il n'y a pas d'os.sleep.
  • **os.time() n'est pas l'heure du jour** : il compte les ticks depuis le démarrage de l'ordinateur. L'heure du jour est os.day_time(), de 0 à 23999 (6000 est midi). os.clock() donne les secondes depuis le démarrage. Il n'y a ni os.epoch, ni os.day, ni os.date.
  • os.start_timer(secondes) renvoie un numéro et met plus tard {name = "timer", id = ...} dans la file. Il n'y a pas d'os.cancelTimer : gardez le numéro du minuteur qui vous intéresse et ignorez les autres. Pas d'os.setAlarm non plus : comparez os.day_time() dans une boucle. 256 minuteurs peuvent attendre en même temps.

Temps et minuteries donne les détails.

La vitesse : un budget d'instructions, jamais « too long without yielding »

ComputerCraft tue un programme qui calcule quelques secondes sans rendre la main (Too long without yielding), si bien que les longues boucles ont besoin d'astuces comme sleep(0) ou os.queueEvent. Brass, jamais. Chaque ordinateur exécute un nombre fixe d'instructions par tick ; quand elles sont épuisées, le programme se met en pause là où il est et repart au tick suivant. Un while true do end est sans danger pour le serveur : il n'use que le temps de son propre ordinateur.

ordinateurinstructions par seconde à 256 tr/min
Calculateur à tubes400
Ordinateur à transistors1 600
Mini-ordinateur6 000
Micro-ordinateur24 000
Microcontrôleur8 000
Ordinateur moderne100 000

Le budget suit la rotation de l'arbre qui entraîne l'ordinateur : moitié moins vite à 128 tr/min, rien sans rotation. Les gros travaux de la bibliothèque coûtent des instructions en plus (trier, les longs textes, parcourir un réseau). Deux conséquences : une boucle qui vérifie quelque chose sans arrêt (attente active) gaspille le temps dont le reste du programme a besoin, alors attendez avec os.pull_event ou sleep ; et sur les plus vieux ordinateurs, moins d'instructions veut dire des programmes plus simples. Voir Vitesse, mémoire et limites.

La mémoire en cellules

Un programme ComputerCraft peut utiliser autant de mémoire que le Java du serveur lui en donne. Un ordinateur Brass a une mémoire fixe, comptée en cellules :

ordinateurmémoire
Calculateur à tubes2 048 cellules
Ordinateur à transistors8 192 cellules
Mini-ordinateur32 768 cellules
Micro-ordinateur131 072 cellules
Microcontrôleur16 384 cellules
Ordinateur moderne1 048 576 cellules

Une table coûte 4 cellules plus 2 par entrée, une chaîne 1 cellule plus 1 pour 8 caractères, un appel de fonction 8 cellules tant qu'il dure. Dépasser la capacité arrête le programme sur out of memory. os.memory() renvoie {used = ..., total = ...}, et la commande mem l'affiche. Autres limites fixes : 200 appels imbriqués (stack overflow), 65536 caractères par chaîne, 256 évènements en file, 256 minuteurs en attente. Voir Limites.

Côte à côte

Dans ComputerCraftEn Brass
local ok, err = pcall(f, x)local r = pcall(f, x), puis r.ok, r.value, r.error
return a, breturn {a = a, b = b}
function f(...)function f(liste)
local args = {...} (arguments du programme)arg[1], arg[2]...
des fermetures sur les locales d'une fonctionlocales du fichier, paramètres, tables avec self
setmetatable(obj, {__index = Classe})copier les fonctions dedans : table.copy(Classe)
coroutine, parallel.waitForAnyune boucle d'évènements avec os.pull_event et des minuteurs
s:gsub("a", "b")table.concat(s:split("a"), "b")
s:match("^%s*(.-)%s*$")s:trim()
bit32.band(a, b), a & bde l'arithmétique avec // et %
os.pullEvent("key")os.pull_event("key"), qui renvoie une table
keys.enter"enter" (dans e.key)
os.startTimer(1)os.start_timer(1)
os.cancelTimer(id)ignorer l'évènement du minuteur
os.time() (l'heure du jour)os.day_time() (de 0 à 23999)
os.epoch("utc")pas d'heure réelle : os.time() compte les ticks depuis le démarrage
os.getComputerID()os.id()
os.setComputerLabel("x")os.label("x")
sleep(0.05) / os.sleepsleep(0.05) (un tick)
redstone.setOutput("top", true)rs.set("top", true)
redstone.getAnalogInput("left")rs.get("left")
peripheral.find("monitor")un moniteur affiche l'écran de l'ordinateur : print, simplement
peripheral.getNames()peripheral.list()
term.setTextColor(colors.red)term.set_fg(term.colors.red) (14)
colors.white = 1, colors.black = 32768term.colors.white = 0, term.colors.black = 15
term.setCursorPos(x, y)term.set_cursor(x, y)
local x, y = term.getCursorPos()local c = term.get_cursor()
local w, h = term.getSize()local t = term.get_size()
paintutils.drawFilledBox(...)gfx.rect(x, y, l, h, couleur, true)
fs.open(p, "r").readAll()fs.read(p)
fs.open(p, "w") + write + closefs.write(p, texte)
fs.makeDir(p)fs.make_dir(p)
rednet.open("back")rien
rednet.send(id, msg, "proto")net.send(id, msg, "proto")
rednet.receive(nil, 5)net.receive(5), qui renvoie une table ou nil
textutils.serialize(t)pas d'équivalent : net envoie les tables telles quelles
shell.run("prog")import "prog"
lua (l'invite interactive)brass
startup.luastartup
« Too long without yielding »jamais : le budget met le programme en pause puis le reprend
mémoire illimitéede 2 K à 1 M cellules, out of memory

Voir aussi