Increíbles hazañas de Clang neutralización de errores

Original web-page: http://blog.llvm.org/2010/04/amazing-feats-of-clang-error-recovery.html

Lunes 5 de abril de 2010

Chris Lattner

Además de analizar y generar código de máquina para sus archivos de origen cuando es válido, el trabajo de la interfaz de un compilador también es detectar el código no válido y darle una pista que explica qué está mal para que pueda solucionar el problema. El error podría ser totalmente inválido (un error) o simplemente podría ser algo que es legal pero parece realmente dudoso (una advertencia). Estos errores y advertencias se conocen como “diagnósticos” del compilador, y Clang pretende ir más allá de lo requerido para brindar una experiencia realmente sorprendente.

Tras el descanso, se muestran algunos ejemplos de áreas en las que Clang trata con especial dureza. Para otros ejemplos, la página web Clang también tiene una página de diagnóstico y Doug mostró cómo Clang diagnostica problemas de búsqueda de nombres de dos fases en un post previo.

Actualización: otras personas están empezando a comparar su compilador favorito. Aquí está el compilador OpenVMS. Chris correo electrónico si usted tiene una comparación desea publicado.

Estos ejemplos utilizan Apple GCC 4.2 como una comparación en estos ejemplos, pero esto no pretende golpear a (una versión antigua de) GCC. Muchos compiladores tienen este tipo de problemas y le recomendamos que pruebe los ejemplos en su compilador favorito para ver cómo funciona. Todos los ejemplos que se muestran son necesariamente pequeños (reducidos), ejemplos que demuestran un problema, cuando los ves en la vida real, a menudo son mucho más convincentes :).

Nombres tipográficos desconocidos

Una cosa molesta de analizar C y C++ es que debes saber qué es un nombre de tipo para analizar el código. Por ejemplo, “(x)(y)” puede ser una conversión de la expresión “(y)” para escribir “x” o podría ser una llamada de la función “x” con la lista de argumentos “(y)”, dependiendo de si x es un tipo o no. Desafortunadamente, un error común es olvidarse de incluir un archivo de encabezado, lo que significa que el compilador realmente no tiene idea de si algo es un tipo o no, y por lo tanto tiene que hacer una estimación basada en el contexto. Aqui hay un par de ejemplos:

$ 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

y:

$ 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.)

Este tipo de cosas también sucede en C si olvida usar ‘struct stat’ en lugar de ‘stat’. Como es un tema común en esta publicación, recuperarse bien al inferir lo que significó el programador ayuda a Clang a evitar emitir errores falsos de seguimiento como las tres líneas que emite GCC en la línea 2.

Corrector ortográfico

Uno de los más visibles cosas que incluye Clang es un corrector ortográfico (también en reddit). Las patadas corrector ortográfico de cuando se utiliza un identificador que Clang no sabe: comprueba contra otros identificadores cercanos y sugiere lo que probablemente significaba. Aquí están algunos ejemplos:

$ cat t.c
#include <inttypes.h>
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'

otro ejemplo es:

$ cat t.c
#include <sys/stat.h>
int foo(int x, struct stat *P) { 
  return P->st_blocksize*2;
}
$ 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'

 

La gran cosa sobre el corrector ortográfico es que las capturas de una amplia variedad de errores comunes, y también ayuda en la recuperación posterior. El código que utiliza más adelante ‘x’, por ejemplo, sabe que se ha declarado como un int64_t, por lo que no conduce a otra extraña seguimiento de errores que no tienen ningún sentido. Tañido utiliza la conocida Función de distancia Levenshtein para calcular el mejor partido de los posibles candidatos.

Definición de tipo de seguimiento

Clang realiza un seguimiento de las definiciones de tipo que escribe en su código con cuidado para que pueda relacionar los errores con los tipos que utiliza en su código. Esto le permite imprimir mensajes de error en sus términos, no en términos de compilación instanciados totalmente resueltos y con plantilla. También utiliza la información de su rango y el caret para mostrarle lo que escribió en lugar de intentar imprimirlo de nuevo con usted. Hay varios ejemplos de esto en la página de diagnósticos de Clang, pero un ejemplo más no puede hacer daño:

$ 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'

Esto muestra que clang le da los nombres de origen a medida que los escribió (“foo::x” y “bar::y”, respectivamente) pero también desenvuelve el tipo y con “aka” en caso de que la representación subyacente sea importante. Otros compiladores suelen proporcionar información completamente inútil que realmente no le dice cuál es el problema. Este es un ejemplo sorprendentemente conciso de GCC, pero también parece faltar información crítica (como por qué no hay coincidencia). Además, si la expresión era más que una simple “a+b”, puedes imaginarte que imprimirlo de nuevo no es lo más útil.

El más irritante analizar gramaticalmente

Un error que cometen muchos programadores principiantes es que accidentalmente definen funciones en lugar de objetos en la pila. Esto se debe a una ambigüedad en la gramática de C++ que se resuelve de manera arbitraria. Esta es una parte inevitable de C++, pero al menos el compilador debería ayudarlo a comprender qué está mal. Aquí hay un ejemplo 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::allo

Me encuentro con esto cuando originalmente declaré que el vector tomaba algunos argumentos (por ejemplo, “10” para especificar un tamaño inicial), pero refactorizo el código y lo eliminé. Por supuesto, si no elimina los paréntesis, el código en realidad está declarando una función, no una variable.

Aquí puede ver que Clang señala claramente que hemos ido y declarado una función (incluso ofrece ayuda para llamarla en caso de que la haya olvidado). GCC, por otro lado, está confundido irremediablemente sobre lo que estás haciendo, pero también escupe un gran nombre tipográfico que no escribiste (¿de dónde vino std::allocator?). Es triste pero cierto que ser un programador experimentado en C++ realmente significa que eres un experto en descifrar los mensajes de error que tu compilador te lanza.

Si continúas probando el ejemplo más clásico donde esto muerde a la gente, puedes ver a Clang esforzarse aún 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 este caso, el segundo error de Clang no es particularmente bueno (aunque da nombres de tipo mucho más concisos), pero da una advertencia realmente crítica, que le dice que los parámetros en el ejemplo están declarando una función, no siendo utilizados como parens para una discusión.

Los puntos y comas que faltan

Un error que frecuentemente cometo (tal vez debido a la gramática muy inconsistente de C++, o tal vez porque soy descuidado y tengo un período de atención corto…) es dejar caer un punto y coma. Afortunadamente, estos son bastante triviales de solucionar una vez que sepas lo que está sucediendo, pero pueden llevar a algunos mensajes de error realmente confusos de algunos compiladores. Esto sucede incluso en los casos en que es inmediatamente obvio lo que le está pasando a un humano (¡si está prestando atención!). Por ejemplo:

$ 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

Tenga en cuenta que GCC emite el error en lo que  sigue el problema. Si la estructura era la última cosa en el extremo de una cabecera, esto significa que usted va a terminar encima de conseguir el mensaje de error en un archivo completamente diferente de donde reside el problema. Este problema también compuestos en sí en C++ (al igual que muchos otros), por ejemplo:

$ cat t2.cc
template<class t>
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

Además de emitir el error confuso “múltiples tipos en una declaración”, GCC continúa confundiéndose de otras maneras.

. vs -> Thinko

En el código C++, los punteros y las referencias a menudo se usan indistintamente y su uso es común. donde te refieres ->. Clang reconoce este tipo de error común y te ayuda a salir:

$ 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::pai

Además de informarle de manera útil que su puntero es un “tipo no de clase”, se esfuerza por deletrear la definición completa de std::map out, lo que ciertamente no es útil.

:: vs : Typo

Tal vez sea solo yo, pero tiendo a cometer este error bastante, de nuevo cuando tengo prisa. El C++  operador :: se usa para separar los especificadores de nombres anidados, pero de alguna manera sigo escribiendo : en su lugar. Aquí hay un ejemplo mínimo que muestra 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

Además de obtener el mensaje de error correcto (y sugerir un reemplazo de fixit a “::”), Clang “sabe lo que quiere decir”, por lo que maneja los usos subsiguientes de a2 correctamente. GCC, en cambio, se confunde acerca de cuál es el error, lo que lo lleva a emitir errores falsos en cada uso de a2. Esto se puede ver con un ejemplo ligeramente elaborado:

$ 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í puede ver que el segundo mensaje de error de Clang es exactamente correcto (y se explica). GCC da un mensaje de seguimiento confuso sobre la conversión de un “int” a x::a. ¿De dónde vino “int”?

Ayudando en situaciones casi sin esperanza

C++ es una herramienta poderosa que te da mucha cuerda para dispararte en el pie y mezclar tus metáforas de múltiples paradigmas. Desafortunadamente, este poder te brinda muchas oportunidades para encontrarte en una situación casi desesperada en la que sabes que “algo está mal” pero no tienes idea de cuál es el problema real o cómo solucionarlo. Afortunadamente, Clang intenta estar ahí para ti, incluso en los momentos más difíciles. Por ejemplo, aquí hay un caso que involucra una búsqueda 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 este caso, se puede ver que no sólo te dice sonido metálico que hay una ambigüedad, que te dice exactamente los caminos a través de la jerarquía de herencia que son los problemas. Cuando usted está tratando con una jerarquía no trivial, y todas las clases no están en un solo archivo mirando a usted, esto puede ser un protector de la vida real.

Para ser justos, GCC de vez en cuando trata de ayudar. Desafortunadamente, cuando lo hace, no está claro si ayuda más de lo que duele. Por ejemplo, si comenta las dos declaraciones de uso en el ejemplo anterior, obtendrá:

$ 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()

Parece que GCC está intentando aquí, pero ¿Por qué está emitiendo dos errores en la línea 10 y por qué está imprimiendo B1::f dos veces en cada una? Cuando recibo este tipo de errores (lo cual es bastante raro, ya que no uso la herencia múltiple como esta a menudo), realmente valoro la claridad al desentrañar lo que está sucediendo.

Una cosa más… los conflictos de fusión

Bueno, esto puede estar yendo un poco lejos, pero ¿De qué otra manera te vas a enamorar completamente de 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 realmente detecta el conflicto de fusión y analiza un lado del conflicto. No quieres obtener un montón de tonterías de tu compilador en un error tan simple, ¿verdad?

Clang: diseñado para programadores reales que pueden cometer errores ocasionales. ¿Por qué conformarse con menos?

-Chris

About the Author