57

Ответ на пост «Потому что кожаные должны страдать?»1

Доступно об АйТи: Почему Python сам не может добавить кавычки?

У меня есть две серии, «Детские вопросы» и «Доступно об АйТи» — вопрос подходит к обеим.

Мем, вызвавший мою заметку

Мем, вызвавший мою заметку

Вкратце: в спецификации языка программирования очень подробным образом описано, какая программа корректна, а какая нет. Но спецификация совершенно не говорит, что делать при ошибке, и компилятор вправе подсказать человеческим языком, чего не хватает. Но незаметно «помочь», то есть принять как корректную — грубое нарушение.

А теперь давайте расскажу, как происходит разбор любого языка.

Я не настолько силён в Python, писать простенькие скрипты могу, но синтаксис ещё не засел в подкорку — так что разрешите за пример брать Паскаль и Си. Начнём со строчки Паскаля (не совсем стандартного, скорее Delphi, но пусть будет).

procedure Print(x : string = '');

Для начала программа производит лексический анализ — разбирает программу на знаки и слова. Слова пишем большими буквами, потому что Паскалю регистр не важен (некогда это было вопросом кроссплатформенности).

ключевое слово PROCEDURE
идентификатор (имя) PRINT
знак (
идентификатор X
знак :
идентификатор STRING
знак =
строка пустая
знак )
знак ;

Этот поток слов и знаков идёт на синтаксический анализ, и он происходит так.

  1. Видим ключевое слово PROCEDURE, переходим в режим «заголовок процедуры».

  2. Видим идентификатор PRINT, это название процедуры.

  3. Видим знак (, переходим в режим «список параметров».

  4. Видим идентификатор X, переходим в режим «однотипные параметры».

  5. Видим знак :, переходим в режим «тип».

  6. В режиме «тип» получается считать только идентификатор STRING.

  7. В режиме «однотипные параметры» видим знак равенства и считываем значение по умолчанию (пустую строку), разрешите дальше не расписывать.

Вот этот разбор «видим-переходим» самый простой и пишется опытным программистом по наитию.

Язык Си действует сложнее, аналогичную строку

void print(char* x = "")

Си начинает понимать, что здесь написано, когда увидит круглую скобку, и только тогда он говорит: это заголовок функции. Потом возвращается назад и смотрит, что было до этого. Как вы видите, уже есть элементы разбора текста справа налево. Разбор таких языков часто программирует автоматика по формальному описанию языка, примерно такому:

<direct-declarator> ::= <identifier>
| ( <declarator> )
| <direct-declarator> [ {<constant-expression>}? ]
| <direct-declarator> ( <parameter-type-list> )
| <direct-declarator> ( {<identifier>}* )

(специально нашёл именно тот кусок языка Си, что относится к нашей строчке.)

Другими словами, за разбором компьютерных языков выстроена немаленькая теория.

А что будет, если язык будет подчищать за человеком такие ошибки?

Первое. Часто подобные предположения неоднозначны. Возьмём процедуру посложнее:

procedure Print(x : string = ''; y : integer = 0);

…и вызовем её Print('text, 10); Оба места, где можно поставить закрывающуюся кавычку — после text или после 10 — дают корректный вызов. А может, программист вообще не хотел открывать кавычку и text — это чьё-то имя (идентификатор)?

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

Третье. Если окидывать постоянно, начнётся такое: при удлинении текста вдвое время сборки повысится вчетверо. Мой хобби-проект «Юникодия» (только собственные файлы, написанные человеком — без библиотек, программно генерируемых и файлов данных) занимает 1,2 мегабайта на языке Си++. Мой рабочий проект, который пишется бригадой примерно из 15 прогеров,— сотни мегабайт. Компиляция таких монстров будет занимать вечность!

Ускоритель компиляции Си++ под названием Unity (не путать с одноимённым игровым движком!) работает так: когда программа состоит из тысячи модулей, он объединяет их по 10, и получается 100 штук. Работает Unity именно потому, что в Си++ всё наоборот: один длинный модуль компилируется быстрее десяти коротких.

Четвёртое. Это бессмысленно удлиняет спецификацию, а главное — стройная теория формальных языков, которую задел по поверхности, перестаёт работать. Даже если условный Бьярне Гослинг (комбинация имён Бьярне Строуструп, автор Си++, и Джеймс Гослинг, автор Java) напишет свой личный язык с таким сервисом, существует множество программ более тупых, чем компиляторы, которым, тем не менее, нужен корректный исходный текст.

  • Начнём с форматёров — они берут исходный текст и расставляют в нём отступы в соответствии с принятой в конторе системой.

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

  • В ту же степь — вышеупомянутый ускоритель Unity.

  • Есть система локализации Gettext — она просматривает программу на предмет строк и спрашивает у программиста: какие из них подлежат переводу? Те, что подлежат, она вносит в языковый ресурс.

Пятое. А это уже реальный случай с языком Go от Google. Языки типа Паскаля, к которым относится и Go, имеют свободный синтаксис (расстановка пробелов и переводов строк не важна). Такие языки традиционно после каждого оператора ставят точку с запятой, и чтобы избавиться от «рака точек с запятой» и в то же время лучше задействовать доступный инструментарий, они решили автоматически расставлять точки с запятой ещё до лексического анализа — именно так, перевод строки не внесён в синтаксис языка!

Привело это к тому, что годятся не все стили текста.

func f() { // Этот стиль работает

}

func g() // А этот нет — тут автомат ложно поставит точку с запятой

{

}

Вот как-то так, спасибо за внимание!

4
Автор поста оценил этот комментарий

Она же и единственная. Все остальное не важно.

И дело даже не в неоднозначности. Однозначные, вроде бы, ситуации тоже нельзя исправлять. Если человек пропустил кавычку - что еще он мог пропустить?

То, что можно исправлять - это варнинги, а не ошибки. Например, возможная потеря знака.

раскрыть ветку (1)
1
Автор поста оценил этот комментарий

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

показать ответы
2
Автор поста оценил этот комментарий

Да спасибо уже из-за таких вот наделали auto-complete хероту, которую пол года искать где вырубать, которая "догадывается" и подставляет закрытия когда надо и когда не надо. Причем для каких олигофренов это нужно - не понятно.

раскрыть ветку (1)
0
Автор поста оценил этот комментарий

Всё сильно зависит от языка и стиля программирования. На языках со статической типизацией, если ещё и типы угадывает, то помогает.

Вот этот CLangD тормозит как чёрт, но у него с этим неплохо.

Автор поста оценил этот комментарий

И что все это меняет? Делает ошибку не ошибкой?

раскрыть ветку (1)
0
Автор поста оценил этот комментарий

Приходится балансировать одно из трёх:

• либо объявить операцию нежелательной — а если в ней есть смысл, то и придумать, как заткнуть предупреждение;

• либо пропускать без вопросов;

• либо полностью запретить.

На четвёртое спецификация языка не способна.

Например, Java приняла решение при любом сужении точности выдавать ошибку. Но все эти расчёты, особенно на ограниченных платформах вроде ME, иногда заедают.

Автор поста оценил этот комментарий

Еще раз: чем отличается преобразование целых типов с потерей данных от преобразования типов указателей?

И то и другое, вообще говоря, некорректно.

И то и другое разрешается при явном указании на то, что так и задумано.

Просто последствия второго более катастрофичны. Поэтому отслеживание более жесткое. Единственное различие.

Варнинг - тоже ошибка.

раскрыть ветку (1)
0
Автор поста оценил этот комментарий

Преобразовать целый тип с потерей данных — нередкая ситуация. Пример: x86 на продвинутой ОС, поток-память. У виртуального потока счётчик — самый длинный из доступных типов, у памяти — ну понятно, size_t.


Другой пример — расчёты в кодировке символов, что-то вроде '0' + digit. Какой у этой штуки тип? — Int, ведь любая арифметическая операция производит как минимум его!


Преобразовать тип указателя вверх по иерархии — стандартная ситуация.


Вниз по иерархии — частая, но подозрительная. Например, знаменитый паттерн программирования «Public Морозов», когда исходный объект скрысил в protected важную функцию.


В несвязанный (ни вверх, ни вниз) — аномальная ситуация и всегда хак.

показать ответы
0
Автор поста оценил этот комментарий

Да, без варнингов было под х86. Под х64 - тот же варнинг про потерю данных. Ошибки нет.

Но что это меняет?

раскрыть ветку (1)
0
Автор поста оценил этот комментарий

M$ это делает предупреждением, потому что это стандартная ошибка перехода на x64. Очень много ложных тревог и мне больше импонирует подход G++, но я их понимаю: сам две недели переходил на x64 и месяц ловил баги.


Предупреждение — это «программа корректна и я не вправе запретить компиляцию, но подозреваю ошибку».

показать ответы
1
Автор поста оценил этот комментарий

Естественно, 64. 32 сейчас еще поискать.

Вы не согласны, что все приведенные мной строки ошибочны?

Компилятор и преобразование указателей не запрещает, если ему явно это запретить.

раскрыть ветку (1)
0
Автор поста оценил этот комментарий

Я имею в виду, под какую платформу сборка идёт.

показать ответы
0
Автор поста оценил этот комментарий

ptrdiff_t a = -5;

int e = a;

Компилится без варнингов. Равно как и с intptr_t.


Если написать с реальным синонимом:

typedef long long qwert;

qwert a = -5;

int e = a;

Тот же варнинг про потерю данных.


Но дело не в этом. Дело в том, что во всех случаях синтаксически и семантически эти конструкции эквивалентны. И во всех - ошибочны. Просто одну ошибку браузер берется исправить (ему не так страшно), а другую - нет. Но, если ему явно написать, что это не ошибка (применив преобразование, которое ничего реально не делает), то он согласится.


Кстати, если написать

int f = (int)a;

варнинга нет.

То есть, поведение идентично преобразованию указателей.

раскрыть ветку (1)
0
Автор поста оценил этот комментарий

Какая архитектура: x86 или x64? Подозреваю, x86, где ptrdiff_t обычно int, реже long того же размера. Я рассказывал, что стандартная ошибка перехода на x64.


Я работаю с другими компиляторами — MinGW и CLang. Там это только в анализе.


Потому и говорю: предупреждения — это места, которые компилятор не вправе запрещать, но похожи на ошибки.


Очень немного предупреждений заспецифицированы, наобум.

• Одна из не слишком удачных вещей Си++ — is_constant_evaluated

• Места, где программист сам просит выдать предупреждение — [[deprecated]], #warning.

• ТРЫ. Некоторые места с совместимостью вроде отсутствия ключевого слова override/final.

показать ответы
0
Автор поста оценил этот комментарий

Типы не подходят во всех строках.

раскрыть ветку (1)
0
Автор поста оценил этот комментарий

Про четвёртую ПАРДОН: Тут просто сужающее преобразование long long → int, и в большинстве случаев оно катит, однако назови long long синонимом ptrdiff_t или intptr_t, и вот уже ошибка перехода на x64.


Почему-то прочитал как «указатель → число», реально 99% ошибка.


Однако возьми другой язык, Java, где более 2 млрд объектов в одном массиве быть не может и точка, зато типизация куда более жёсткая, и преобразование long long → int уже ошибка.

показать ответы
0
Автор поста оценил этот комментарий

Вот код (С++, VS):

long long a = -5;

int *c = &a;

int *d = (int *)&a;

int e = a;

Три последних строки по семантике и синтаксису равнозначны. Но при этом:

Вторая строка - ошибка.

Четвертая - варнинг.

Треться компилится без ничего, хотя в ней делается ровно то же, что и во второй. Просто намерения программиста выражены более явно.

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

В последней это преобразование типа по сути и делается. Потому что ошибка потенциально не настолько разрушительна, как во второй строке.

раскрыть ветку (1)
0
Автор поста оценил этот комментарий

Вторая строка. Не подходят типы, ошибка.

Третья строка. Не подходят типы, но преобразование есть, и программист говорит, что так надо. Оставить.

Четвёртая строка. Старая совместимость с Си (почти всегда недосмотр), да ещё и сужающее преобразование. Почти всегда ошибка. Если такое пишешь на плюсах — reinterpret_cast, и точка.

показать ответы
Автор поста оценил этот комментарий

И что?

раскрыть ветку (1)
0
Автор поста оценил этот комментарий

Вы сказали: «То, что можно исправлять». Нет.

Отвечает синтаксису и семантике, нет формальных причин отказывать в компиляции. С большой вероятностью, неверно.


А ещё есть такой инструмент, как статический анализ кода — это те же предупреждения компилятора, только ещё больше и на каждом шагу.


И вот ложная тревога этого анализа. Когда я испытывал работу конструктора переноса, он писал — «переменная используется после move». Дело было в юнит-тесте, я так хотел — и потому эти предупреждения глушил.

показать ответы
13
Автор поста оценил этот комментарий

Ответ простой: он не знает чего хотел программист.

раскрыть ветку (1)
0
Автор поста оценил этот комментарий

Часто подобные предположения неоднозначны — это моя первая причина.

показать ответы
2
Автор поста оценил этот комментарий

Мог бы легче объяснить все.


Например, есть предложение с ошибками "ты могу бегут".

Явно в предложении ошибка. Теперь попробуй исправь на правильное.

раскрыть ветку (1)
Автор поста оценил этот комментарий

Нет. Это совсем дичь и в любом случае ошибка.

показать ответы