У меня есть две серии, «Детские вопросы» и «Доступно об АйТи» — вопрос подходит к обеим.
Мем, вызвавший мою заметку
Вкратце: в спецификации языка программирования очень подробным образом описано, какая программа корректна, а какая нет. Но спецификация совершенно не говорит, что делать при ошибке, и компилятор вправе подсказать человеческим языком, чего не хватает. Но незаметно «помочь», то есть принять как корректную — грубое нарушение.
А теперь давайте расскажу, как происходит разбор любого языка.
Я не настолько силён в Python, писать простенькие скрипты могу, но синтаксис ещё не засел в подкорку — так что разрешите за пример брать Паскаль и Си. Начнём со строчки Паскаля (не совсем стандартного, скорее Delphi, но пусть будет).
procedure Print(x : string = '');
Для начала программа производит лексический анализ — разбирает программу на знаки и слова. Слова пишем большими буквами, потому что Паскалю регистр не важен (некогда это было вопросом кроссплатформенности).
ключевое слово PROCEDURE
идентификатор (имя) PRINT
знак (
идентификатор X
знак :
идентификатор STRING
знак =
строка пустая
знак )
знак ;
Этот поток слов и знаков идёт на синтаксический анализ, и он происходит так.
Видим ключевое слово PROCEDURE, переходим в режим «заголовок процедуры».
Видим идентификатор PRINT, это название процедуры.
Видим знак (, переходим в режим «список параметров».
Видим идентификатор X, переходим в режим «однотипные параметры».
Видим знак :, переходим в режим «тип».
В режиме «тип» получается считать только идентификатор STRING.
В режиме «однотипные параметры» видим знак равенства и считываем значение по умолчанию (пустую строку), разрешите дальше не расписывать.
Вот этот разбор «видим-переходим» самый простой и пишется опытным программистом по наитию.
Язык Си действует сложнее, аналогичную строку
Си начинает понимать, что здесь написано, когда увидит круглую скобку, и только тогда он говорит: это заголовок функции. Потом возвращается назад и смотрит, что было до этого. Как вы видите, уже есть элементы разбора текста справа налево. Разбор таких языков часто программирует автоматика по формальному описанию языка, примерно такому:
<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() // А этот нет — тут автомат ложно поставит точку с запятой
{
}
Вот как-то так, спасибо за внимание!