La funció Error Recovery de Clang: una gran troballa

Translation into Catalan of an interesting article by Chris Lattner is an American software engineer best known as the main author of LLVM and related projects, such as the Clang compiler and the Swif programming language.

clang compilersoftwarespanish translation
30 March, 2022 A translating about a great piece of software: Clang compiler
30 March, 2022 A translating about a great piece of software: Clang compiler

A translation provided by Chema, a Catalan translator specializing in translations from Englih into Catalan

An original text written by Chris Lattner, originally published in
http://blog.llvm.org/2010/04/amazing-feats-of-clang-error-recovery.html

* * *

A més d’analitzar i generar codi de màquina per als fitxers font quan sigui vàlid, la tasca d’un compilador també és detectar codi no vàlid i donar-vos una pista que expliqui què passa, perquè pugueu solucionar el problema. L’error pot ser directament no vàlid (un error) o pot ser una cosa que és legal però que sembla realment dubtós (un avís). Aquests errors i advertències es coneixen com a “diagnòstics” del compilador, i Clang pretén anar més enllà del deure per oferir una experiència realment sorprenent.

Després del descans, mostrem alguns exemples de zones on Clang s’esforça especialment. Per a altres exemples, la pàgina web de Clang també té una pàgina de diagnòstic i Doug va mostrar com Clang diagnostica problemes de cerca de noms en dues fases en una publicació anterior al bloc.

Actualització: altres persones estan començant a fer compacions amb el seu compilador favorit. Aquí teniu el compilador OpenVMS. Envieu un correu electrònic a Chris si teniu una comparació que voleu publicar.

Aquests exemples utilitzen Apple GCC 4.2 com a comparació d’aquests exemples, però això no pretén atacar (una versió antiga de) GCC. Molts compiladors tenen aquest tipus de problemes i us recomanem que proveu els exemples del vostre compilador preferit per veure com ho fa. Tots els exemples que es mostren són necessàriament exemples petits (reduïts) que demostren un problema, quan els veus a la vida real, sovint són molt més convincents :).

Typenames desconeguts

Una cosa molesta d’analitzar C i C++ és que has de saber què és un
“typename” per analitzar el codi. Per exemple, “(x)(y)” pot ser un model de l’expressió “(y)” per escriure “x” o pot ser una crida de la funció “x” amb la llista d’arguments “(y)”. segons si x és un tipus o no. Malauradament, un error comú és oblidar-se d’incloure un fitxer de capçalera, la qual cosa significa que el compilador realment no té ni idea de si alguna cosa és un tipus o no i, per tant, ha de fer una conjectura fortament educada basada en el context. Aquí teniu un parell d’exemples:

$ cat t.m
NSString *P = @"foo";
$ clang t.m
t.m:4:1: error: unknown type name 'NSString'
NSString *P = @"foo";
^
$ gcc t.m
t.m:4: error: expected '=', ',', ';', 'asm' or 'attribute' before '*' token

i:

$ cat t.c
int foo(int x, pid_t y) {
return x+y;
}
$ clang t.c
t.c:1:16: error: unknown type name 'pid_t'
int foo(int x, pid_t y) {
^
$ gcc t.c
t.c:1: error: expected declaration specifiers or '…' before 'pid_t'
t.c: In function 'foo':
t.c:2: error: 'y' undeclared (first use in this function)
t.c:2: error: (Each undeclared identifier is reported only once
t.c:2: error: for each function it appears in.)

Aquest tipus de coses també succeeixen a C si oblideu fer servir ‘struct stat’ en comptes de ‘stat’. Com és un tema habitual en aquesta publicació, recuperar-se bé deduint què volia dir el programador ajuda a Clang a evitar emetre errors de seguiment falsos com les tres línies que GCC emet a la línia 2.

Corrector ortogràfic

Una de les coses més visibles que inclou Clang és un corrector ortogràfic (també a reddit). El corrector ortogràfic s’activa quan utilitzeu un identificador que Clang no coneix: compara altres identificadors propers i suggereix el que probablement volíeu dir. Aquí teniu uns quants exemples:

$ cat t.c
include
int64 x;
$ clang t.c
t.c:2:1: error: unknown type name 'int64'; did you mean 'int64_t'?
int64 x;
^~~~~
int64_t
$ gcc t.c
t.c:2: error: expected '=', ',', ';', 'asm' or 'attribute' before 'x'

un altre exemple és:

$ cat t.c
include
int foo(int x, struct stat P) { return P->st_blocksize2;
}
$ clang t.c
t.c:4:13: error: no member named 'st_blocksize' in 'struct stat'; did you mean 'st_blksize'?
return P->st_blocksize*2;
^~~~
st_blksize
$ gcc t.c
t.c: In function ‘foo’:
t.c:4: error: 'struct stat' has no member named 'st_blocksize'

El millor del corrector ortogràfic és que detecta una gran varietat d’errors comuns i també ajuda a la recuperació posterior. El codi que més tard va utilitzar “x”, per exemple, sap que es declara com a int64_t, de manera que no condueix a altres errors estranys que no tinguin cap sentit. Clang utilitza la coneguda funció de distància de Levenshtein per calcular la millor coincidència entre els possibles candidats.

Seguiment de Typedef

Clang fa un seguiment dels tipus de definicions que escriviu al vostre codi amb cura perquè pugui relacionar els errors amb els tipus que feu servir al vostre codi. Això li permet imprimir missatges d’error en els vostres termes, no en termes del compilador completament resolts i instància de plantilla. També utilitza la seva informació d’interval i el cursor per mostrar-vos el que heu escrit en lloc d’intentar imprimir-lo de nou. Hi ha diversos exemples d’això a la pàgina de diagnòstic de Clang, però un exemple més no pot fer mal:

$ cat t.cc
namespace foo {
struct x { int y; };
}
namespace bar {
typedef int y;
}
void test() {
foo::x a;
bar::y b;
a + b;
}
$ clang t.cc
t.cc:10:5: error: invalid operands to binary expression ('foo::x' and 'bar::y' (aka 'int'))
a + b;
~ ^ ~
$ gcc t.cc
t.cc: In function 'void test()':
t.cc:10: error: no match for 'operator+' in 'a + b'

Això mostra que clang us dóna els noms de font tal com els heu escrit (“foo::x” i “bar::y”, respectivament), però també desembolica el tipus y amb “aka” en cas que la representació subjacent sigui important. Altres compiladors normalment donen informació completament inútil que no us indica realment quin és el problema. Aquest és un exemple sorprenentment concís de GCC, però també sembla que falta alguna informació crítica (com ara per què no hi ha coincidència). A més, si l’expressió era més d’un sol “a+b”, us podeu imaginar que tornar-vos-la a imprimir no és el més útil.

L’anàlisi més molest

Un error que cometen molts programadors principiants és que defineixen accidentalment funcions en lloc d’objectes a la pila. Això es deu a una ambigüitat en la gramàtica C++ que es resol de manera arbitrària. Aquesta és una part inevitable de C++, però almenys el compilador us hauria d’ajudar a entendre què passa. Aquí teniu un exemple trivial:

$ cat t.cc
#include <vector>

int foo() {
std::vector<std::vector<int> > X();
return X.size();
}
$ clang t.cc
t.cc:5:11: error: base of member reference has function type 'std::vector<std::vector<int> > ()'; perhaps you meant to call this function with '()'?
return X.size();
^
()
$ gcc t.cc
t.cc: In function ‘int foo()’:
t.cc:5: error: request for member ‘size’ in ‘X’, which is of non-class type ‘std::vector<std::vector<int, std::allocator<int> >, std::allocator<std::vector<int, std::allocator<int> > > > ()()’

Em vaig trobar amb això quan originalment vaig declarar que el vector tenia alguns arguments (per exemple, “10” per especificar una mida inicial), però refactoritzar el codi i eliminar-ho. Per descomptat, si no elimineu els parèntesis, el codi en realitat declara una funció, no una variable.

Aquí podeu veure que Clang assenyala amb força claredat que hem anat a declarar una funció (fins i tot ofereix ajudar-vos a trucar-la en cas que us oblideu de ()). GCC, d’altra banda, està desesperadament confós sobre el que esteu fent, però també emet un nom de tipus gran que no heu escrit (d’on prové std::allocator?). És trist, però cert que ser un programador C++ experimentat significa realment que sou hàbils a desxifrar els missatges d’error que us envia el vostre compilador.

Si continueu provant l’exemple més clàssic on això mossega a la gent, podeu veure que Clang s’esforça encara més:

$ cat t.cc
#include <fstream>
#include <vector>
#include <iterator>

int main() {
std::ifstream ifs("file.txt");
std::vector<char> v(std::istream_iterator<char>(ifs),
std::istream_iterator<char>());

std::vector<char>::const_iterator it = v.begin();
return 0;
}
$ clang t.cc
t.cc:8:23: warning: parentheses were disambiguated as a function declarator
std::vector<char> v(std::istream_iterator<char>(ifs),
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
t.cc:11:45: error: member reference base type 'std::vector<char> (*)(std::istream_iterator<char>, std::istream_iterator<char> (*)())' is not a structure or union
std::vector<char>::const_iterator it = v.begin();
~ ^
$ gcc t.cc
t.cc: In function ‘int main()’:
t.cc:11: error: request for member ‘begin’ in ‘v’, which is of non-class type
‘std::vector<char, std::allocator<char> > ()(std::istream_iterator<char, char, std::char_traits<char>, long int>, std::istream_iterator<char, char, std::char_traits<char>, long int> (*)())’

En aquest cas, el segon error de Clang no és especialment gran (tot i que dóna noms de tipus molt més concisos), però dóna un avís molt crític, que us diu que els parèntesis de l’exemple estan declarant una funció, no s’utilitzen com a pares. per un argument.

Falten punts i coma

Un error que faig sovint (potser a causa de la gramàtica molt inconsistent de C++, o potser perquè sóc descuidat i tinc una capacitat d’atenció curta…) és deixar anar un punt i coma. Afortunadament, són bastant trivials d’arreglar un cop sabeu què està passant, però poden provocar missatges d’error realment confusos d’alguns compiladors. Això passa fins i tot en els casos en què és immediatament obvi què li està passant a un humà (si està prestant atenció!). Per exemple:

$ cat t.c
struct foo { int x; }

typedef int bar;
$ clang t.c
t.c:1:22: error: expected ';' after struct
struct foo { int x; }
^
;
$ gcc t.c
t.c:3: error: two or more data types in declaration specifiers

Tingueu en compte que GCC emet l’error en el que segueix el problema. Si l’estructura era l’última cosa al final d’una capçalera, això vol dir que acabareu rebent el missatge d’error en un fitxer completament diferent del que es troba el problema. Aquest problema també es composa en C++ (com ho fan molts altres), per exemple:

$ cat t2.cc
template
class a{}

class temp{};
a<temp> b;

class b {
}
$ clang t2.cc
t2.cc:2:10: error: expected ';' after class
class a{}
^
;
t2.cc:8:2: error: expected ';' after class
}
^
;
$ gcc t2.c
t2.cc:4: error: multiple types in one declaration
t2.cc:5: error: non-template type ‘a’ used as a template
t2.cc:5: error: invalid type in declaration before ‘;’ token
t2.cc:8: error: expected unqualified-id at end of input

+-A més d’emetre l’error confús “diversos tipus en una declaració”, GCC passa a confondre’s d’altres maneres.

. vs -> Thinko

En el codi C++, els punters i les referències sovint s’utilitzen de manera força intercanviable i és habitual utilitzar . on vols dir ->. Clang reconeix aquest tipus d’error comú i t’ajuda a:

$ cat t.cc
#include <map>

int bar(std::map<int, float> *X) {
return X.empty();
}
$ clang t.cc
t.cc:4:11: error: member reference type 'std::map<int, float> *' is a pointer; maybe you meant to use '->'?
return X.empty();
~^
->
$ gcc t.cc
t.cc: In function ‘int bar(std::map<int, float, std::less<int>, std::allocator<std::pair<const int, float> > >*)’:
t.cc:4: error: request for member ‘empty’ in ‘X’, which is of non-class type ‘std::map<int, float, std::less<int>, std::allocator<std::pair<const int, float> > >*’

A més d’informar-vos de manera útil que el vostre punter és un “tipus que no és de classe”, fa tot el possible per escriure la definició completa de std::map out, que certament no és útil.

:: vs: Typo

Potser sóc jo, però tendeixo a cometre aquest error bastant, de nou amb pressa. L’operador C++ :: s’utilitza per separar els especificadors de noms imbricats, però d’alguna manera segueixo escrivint :. Aquí teniu un exemple mínim que mostra la idea:

$ cat t.cc
namespace x {
struct a { };
}

x:a a2;
x::a a3 = a2;
$ clang t.cc
t.cc:5:2: error: unexpected ':' in nested name specifier
x:a a2;
^
::
$ gcc t.cc
t.cc:5: error: function definition does not declare parameters
t.cc:6: error: ‘a2’ was not declared in this scope

A més d’encertar el missatge d’error (i suggerir una substitució de fixit a “::”), Clang “sap què vols dir”, de manera que gestiona correctament els usos posteriors de a2. GCC, en canvi, es confon sobre quin és l’error que el porta a emetre errors falsos en cada ús d’a2. Això es pot veure amb un exemple una mica elaborat:

$ cat t2.cc
namespace x {
struct a { };
}

template <typename t>
class foo {
};

foo<x::a> a1;
foo<x:a> a2;

x::a a3 = a2;
$ clang t2.cc
t2.cc:10:6: error: unexpected ':' in nested name specifier
foo<x:a> a2;
^
::
t2.cc:12:6: error: no viable conversion from 'foo<x::a>' to 'x::a'
x::a a3 = a2;
^ ~~
t2.cc:2:10: note: candidate constructor (the implicit copy constructor) not viable: no known conversion from 'foo<x::a>' to 'x::a const' for 1st argument
struct a { };
^
$ gcc t2.cc
t2.cc:10: error: template argument 1 is invalid
t2.cc:10: error: invalid type in declaration before ‘;’ token
t2.cc:12: error: conversion from ‘int’ to non-scalar type ‘x::a’ requested

Aquí podeu veure que el segon missatge d’error de Clang és exactament correcte (i s’explica). GCC ofereix un missatge de seguiment confús sobre la conversió d’un “int” a x::a. D’on prové “int”?

Ajudar en situacions gairebé sense esperança

C++ és una eina elèctrica que us ofereix molta corda per disparar-vos al peu i barrejar les vostres metàfores multi-paradigmàtiques. Malauradament, aquest poder us ofereix moltes oportunitats per trobar-vos en una situació gairebé desesperada en què sabeu que “alguna cosa no funciona”, però no teniu ni idea de quin és el problema real ni de com solucionar-lo. Afortunadament, Clang intenta ser-hi per a tu, fins i tot en els moments més difícils. Per exemple, aquí hi ha un cas que implica una cerca ambigua:

$ cat t.cc
struct B1 { void f(); };
struct B2 { void f(double); };

struct I1 : B1 { };
struct I2 : B1 { };

struct D: I1, I2, B2 {
using B1::f; using B2::f;
void g() {
f();
}
};
$ clang t.cc
t.cc:10:5: error: ambiguous conversion from derived class 'D' to base class 'B1':
struct D -> struct I1 -> struct B1
struct D -> struct I2 -> struct B1
f();
^
$ gcc t.cc
t.cc: In member function ‘void D::g()’:
t.cc:10: error: ‘B1’ is an ambiguous base of ‘D’

En aquest cas, podeu veure que el clang no només us diu que hi ha una ambigüitat, sinó que us indica exactament els camins a través de la jerarquia d’herència que són els problemes. Quan es tracta d’una jerarquia no trivial, i totes les classes no es troben en un sol fitxer mirant-vos, això pot ser un veritable estalvi de vida.

Per ser justos, GCC de tant en tant intenta ajudar. Malauradament, quan ho fa, no està clar si ajuda més del que fa mal. Per exemple, si comenteu els dos amb declaracions de l’exemple anterior, obtindreu:

$ clang t.cc
t.cc:10:5: error: non-static member 'f' found in multiple base-class subobjects of type 'B1':
struct D -> struct I1 -> struct B1
struct D -> struct I2 -> struct B1
f();
^
t.cc:1:18: note: member found by ambiguous name lookup
struct B1 { void f(); };
^
$ gcc t.cc
t.cc: In member function ‘void D::g()’:
t.cc:10: error: reference to ‘f’ is ambiguous
t.cc:2: error: candidates are: void B2::f(double)
t.cc:1: error: void B1::f()
t.cc:1: error: void B1::f()
t.cc:10: error: reference to ‘f’ is ambiguous
t.cc:2: error: candidates are: void B2::f(double)
t.cc:1: error: void B1::f()
t.cc:1: error: void B1::f()

Sembla que GCC ho està intentant aquí, però per què emet dos errors a la línia 10 i per què imprimeix B1::f dues vegades a cadascuna? Quan rebo aquest tipus d’errors (cosa força rara, ja que no faig servir l’herència múltiple com aquesta sovint), valoro molt la claredat a l’hora de desentranyar el que està passant.

Una cosa més… fusionar conflictes

D’acord, això pot anar una mica lluny, però de quina altra manera us enamorareu completament d’un compilador?

$ cat t.c
void f0() {
<<<<<<< HEAD
int x;
=======
int y;
>>>>>>> whatever
}
$ clang t.c
t.c:2:1: error: version control conflict marker in file
<<<<<<< HEAD
^
$ gcc t.c
t.c: In function ‘f0’:
t.c:2: error: expected expression before ‘<<’ token
t.c:4: error: expected expression before ‘==’ token
t.c:6: error: expected expression before ‘>>’ token

Sí, clang detecta realment el conflicte de combinació i analitza una de les parts del conflicte. No voleu obtenir tones de tonteries del vostre compilador en un error tan senzill, oi?

Clang: dissenyat per a programadors reals que poden cometre errors ocasionals. Per què conformar-se amb menys?

Valora este artículo