Удивительные трюки восстановления ошибок Clang

Перевод статьи Amazing feats of Clang Error Recovery, автор - Chris Lattner

Помимо синтаксического анализа и генерации машинного кода для исходных файлов на проверку действительности, работа компилятора FRONTEND - обнаружить неверный код и дать вам подсказку, которая объясняет, что это неправильно, вследствие вы можете решить эту проблему. Ошибка может быть либо прямой вверх недействительным (ошибка) или может быть просто то, что является законным, но выглядит очень сомнительным (предупреждение). Эти ошибки и предупреждения известны как компилятор "диагностика", и Clang стремится идти выше и вне служебного долга, чтобы обеспечить действительно удивительный опыт. После перерыва мы покажем некоторые примеры областей, где Clang особенно сильно старается. Для других примеров, веб - страница Clang также имеет страницу по диагностике и Дуг показал, как Clang диагностирует проблемы поиска имен двухфазные в предварительной записи в блоге.

Обновление: Другие люди начинают сравнивать их любимый компилятор. Вот OpenVMS компилятор. Адрес электронной почты Криса, если у вас есть сравнение вы хотите разм.

Эти примеры используют Apple, GCC 4.2 в качестве сравнения на этих примерах, но это не означает, чтобы колотить (старая версия) НКУ. Многие компиляторы имеют такого рода вопросы, и мы настоятельно рекомендуем вам попробовать примеры на ваш любимый компилятор, чтобы увидеть, как он делает. Примеры все показанные обязательно маленькие (уменьшенные) примеры, которые демонстрируют проблему, когда вы видите их в реальной жизни, они зачастую гораздо более убедительными :).

Неизвестные имена типов

Одна раздражающая вещь о разборе С и C ++ является то, что вы должны знать, что такое Ьурепате для того, чтобы разобрать код. Например, "(х) (у)" может быть либо слепок выражения "(у)" набрать "х", или это может быть вызов функции "х" с "(у)" списка аргументов, в зависимости от того, х является типом или нет. К сожалению, общая ошибка состоит в том, чтобы забыть включить файл заголовка, а это значит, что компилятор действительно понятия не имеет, что-то, является ли тип или нет, и, следовательно, должен сделать сильно обоснованное предположение, основываясь на контексте. Вот несколько примеров:

$ 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

а также:

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

Такого рода вещи также происходит в C, если вы забыли использовать ''struct stat' вместо 'stat'. Как общая тема в этом посте, поправляется от выводя что имел в виду программист помогает Clang избежать излучающие поддельным последующих на ошибках, как три линии НКУ, излучающий на линии 2.

Программа проверки орфографии

Одним из наиболее заметных вещей, которые включает в себя Clang является проверка орфографии (также на Reddit). Средство проверки орфографии в кайф, когда вы используете идентификатор, который Clang не знает: он проверяет против других близких идентификаторов и наводит на мысль, что вы, вероятно, имел в виду. Вот несколько примеров:

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

Другим примером является:

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

Самое замечательное проверки орфографии является то, что он ловит широкий спектр наиболее распространенных ошибок, и это также помогает в дальнейшем восстановление. Код, который позже использовал 'х', например, знает, что он объявлен как int64_t, так что это не приводит к другим странным следовать по ошибкам, которые не имеет никакого смысла. Clang использует хорошо известную функцию расстояния Левенштейна для вычисления наилучшего совпадения из возможных кандидатов.

Typedef Отслеживание

Clang отслеживает вы пишете определений типов в коде так тщательно, что он может относиться к ошибкам типов, которые вы используете в вашем коде. Это позволяет распечатывать сообщения об ошибках в ваших терминах, а не в полной мере решены и шаблон экземпляра термины компилятора. Он также использует информацию диапазона и каретку, чтобы показать вам, что вы написали вместо того, чтобы напечатать его обратно на вас. Есть несколько примеров этого на странице Clang диагностики, но еще один пример не может повредить:

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

Это показывает, что лязг дает имена источников, как вы ввели их ( "Foo :: х" и "бар :: у", соответственно), но он также разворачивает тип у с "ака" в случае, если основное представление имеет важное значение. Другие компиляторы, как правило, дают совершенно бесполезную информацию, которая на самом деле не сказать вам, в чем проблема. Это удивительно кратким пример из GCC, но и, похоже, не хватает какой-то важной информации (например, почему нет никакого совпадения). Кроме того, если выражение было больше, чем один "A + B", вы можете себе представить, что довольно напечатать его на вас не самый полезный.

Раздосадовать Анализировать

Одна ошибка начинающих много программистов ошибка состоит в том, что они случайно определить функции вместо объектов в стеке. Это происходит из-за неоднозначности в грамматике С ++, который разрешен произвольным образом. Это неизбежная часть C ++, но, по крайней мере, компилятор должен помочь вам понять, что происходит не так. Вот простой пример:

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

Я бегу в эту вещь, когда я первоначально объявил вектор, как принимать какие - то аргументы (например, "10", чтобы указать начальный размер), но реорганизовать код и устранить это. Конечно, если вы не удалите скобки, код на самом деле объявление функции, а не переменная. Здесь вы можете увидеть, что Clang указывает достаточно четко, что мы пошли и объявили функцию (она даже предлагает, чтобы помочь вам позвонить это в случае, если вы забыли () 'ы). НКУ, с другой стороны, это и безнадежно запутался о том, что вы делаете, но и извергает большой TypeName, что ты не писал (откуда Std :: аллокатора пришел?). Это печально, но это правда, что, будучи опытным программистом C ++ на самом деле означает, что вы искусны в decyphering сообщения об ошибках, что ваш компилятор изрыгает на вас. Если вы идете попробовать более классический пример, когда это кусает людей, вы можете увидеть Clang попробовать даже Сильнее:

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

В этом случае вторая ошибка лязгом не является особенно велика (хотя это действительно дает гораздо более краткие имена типов), но это дает действительно критическое предупреждение, сообщая вам, что круглые скобки в примере, объявление функции, не используется в качестве скобок для аргумента.

Недостающие Точка с запятой

Одна ошибка, что я часто делают (возможно, из-за дико несовместимым грамматики C ++, или, возможно, потому что я неаккуратно и имеют короткий промежуток внимания...) снижается точка с запятой. К счастью, это довольно тривиально, чтобы исправить, когда вы знаете, что происходит, но они могут привести к некоторым очень запутанных сообщений об ошибках от некоторых компиляторов. Это происходит даже в тех случаях, когда это сразу видно, что происходит с человеком (если они обращают внимание!). Например:

$ 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

Обратите внимание, что GCC выдает ошибку на вещь, которая следует за проблему. Если структура была последняя вещь, в конце заголовка, это означает, что вы будете в конечном итоге получить сообщение об ошибке в совершенно другом файле, чем где проблема лежит. Эта проблема также соединения себя в C ++ (как и многие другие), например:

$ 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

В дополнение к излучающей запутанные ошибку "несколько типов в одной декларации", GCC продолжает запутывать себя другими способами.

. vs -> Thinko

В C ++ код, указатели и ссылки часто привыкают довольно взаимозаменяемыми, и он является общим для использования. где вы имеете в виду ->. Clang признает этот общий вид ошибки и помогает вам:

$ 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> > >*’

Кроме того, чтобы услужливо сообщить вам, что ваш указатель является "внеклассовой типа", он выходит из своего пути к буквам полное определение Std :: наметить, что, безусловно, не полезно.

:: vs : Typo

Может быть, это только у меня, но я, как правило, делают эту ошибку совсем немного, в то время как раз в спешке. C ++ :: Оператор используется для разделения вложенных спецификаторов имен, но так или иначе я продолжаю печатать: вместо этого. Вот минимальный пример, который показывает идею:

$ 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

В дополнение к получению сообщения об ошибке вправо (и предполагая замену FIXIT к "::"), Clang "знает, что вы имеете в виду", так что она обрабатывает последующие использования a2 правильно. НКУ, в отличие от этого, запутывается о том, что ошибка, которая приводит его излучать фиктивные ошибки при каждом использовании a2. Это можно увидеть со слегка разработанным например:

$ 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

Здесь вы можете увидеть, что второе сообщение об ошибке Clang является совершенно верно (и объясняется). GCC дает запутанную следовать по сообщению о преобразовании "Int" х :: а. Откуда "Int" пришел?

Оказание помощи в ближайшую безнадежных ситуациях

C ++ является электрический инструмент, который дает вам много веревки, чтобы стрелять себе в ногу, а также смешивать ваши мульти-paradigmed метафор. К сожалению, эта сила дает вам много возможностей, чтобы найти себя в ближайшем безвыходной ситуации, когда вы знаете, "что-то не так", но не имеют ни малейшего представления о том, что реальная проблема или как ее исправить. К счастью, Clang старается быть там для вас, даже в самые трудные времена. Например, вот случай с участием неоднозначный поиск:

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

В этом случае, вы можете увидеть, что не только лязг вам сказать, что есть неоднозначность, он говорит вам точно пути через иерархии наследования, которые являются проблемы. Когда вы имеете дело с нетривиальной иерархии, и все классы не в одном файле, уставившись на вас, это может быть реальная жизнь заставка. Чтобы быть справедливым, GCC иногда пытается помочь. К сожалению, когда он делает это не ясно, если это помогает больше, чем это больно. Например, если вы закомментировать две с использованием декларации в приведенном выше примере вы получите:

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

Похоже, что GCC пытается здесь, но почему он излучающие две ошибки в строке 10 и почему она печати B1 :: F дважды в каждой из них? Когда я получаю эти рода ошибки (что довольно редко, так как я не использую множественное наследование, как это часто) я действительно ценю ясность при разгадке того, что происходит.

Еще одна вещь... Конфликты при слиянии

Хорошо, это может быть уже слишком, но как еще вы собираетесь упасть полностью в любви с компилятором?

$ 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

Да, на самом деле лязг обнаруживает конфликт слияния и разбирает одну из сторон конфликта. Вы не хотите, чтобы получить тонны ерунды от компилятора на такой простой ошибки, не правда ли? Clang: созданный для настоящих программистов, которые делают может сделать случайную ошибку. Зачем соглашаться на меньшее?