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() // А этот нет — тут автомат ложно поставит точку с запятой

{

}

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

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

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

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

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

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

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

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

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

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

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

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

И что?

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

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

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


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


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

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

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

long long a = -5;

int *c = &a;

int *d = (int *)&a;

int e = a;

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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


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

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

ptrdiff_t a = -5;

int e = a;

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


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

typedef long long qwert;

qwert a = -5;

int e = a;

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


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


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

int f = (int)a;

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

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

раскрыть ветку (9)
Вы смотрите срез комментариев. Чтобы написать комментарий, перейдите к общему списку