refal-5-lambda
refal-5-lambda copied to clipboard
Удаление устаревших элементов языка и компилятора
Мотивация
Проект развивается давно и эволюционно. А любое эволюционное развитие подразумевает накопление рудиментов и атавизмов вплоть до возвратно-гортанного нерва жирафа.
Но в отличие от живых существ, развитие которых осуществляется путём мутаций и отбора (генетическое программирование пока фантастика), программу можно пересмотреть и коренным образом переделать.
Поэтому предлагается пересмотреть язык и компилятор и удалить неадекватные его элементы.
Другой мотивацией может служить, как обычно, ускорение. Ускорение работы самого компилятора + ускорение сборки компилятора. С ускорением работы пока не очевидно, а сборка может ускориться из-за уменьшения объёма исходников.
Перечень устаревших элементов
Ниже будут перечислены возможности, которые предлагается удалить. Или, как минимум, обдумать их удаление.
$DRIVE
, $INLINE
, $SPEC
, -OD
, -OI
, -OS
—
замена на $OPT
и -OT
(#314)
Предлагается под капотом объединить различные виды древесных оптимизаций, как описано в #314. Да, для этого уже создана отдельная задача, здесь упомянуто, поскольку в тему.
$INCLUDE
А вот это уже интереснее! Казалось бы, полезная фича языка и зачем её удалять. Но, рассмотрим её преимущества и недостатки.
- ✔ При использовании
LibraryEx
не нужно явно импортировать используемые функции. В большинстве случаев, этоMap
,MapAccum
иReduce
. - ✔ В подключаемых файлах можно определить функции с атрибутами прогонки/специализации, компилятор их будет оптимизировать без использования
-OG
. Собственно, именно возможность оптимизации была основной мотивацией внедрения$INCLUDE
(#92). Но преимущество раздельной компиляции с древесной оптимизацией не очевидно. При разработке компилятор собирается без оптимизаций для ускорения цикла самоприменения. Дистрибутив собирается редко, при выпуске новой версии, и со всеми оптимизациями — можно и подождать. - ✔ Предобъявления встроенных функций реализованы как неявный
$INCLUDE
. Реализация довольно простая и красивая. Без механизма подключаемых файлов, вероятно, было бы немного сложнее. - (✔) Маленькая приятность — т.к. в каждую единицу трансляции включаются свои локальные функции
Map
и др, по логу профилировщика можно понять, вызовMap
в каком файле требует много времени. Преимущество несущественное. - (✔) Функции
Map
и др. теперь определены черезMu
. Поэтому теперь можно в Рефале-5λ писать<Map Func e.Items>
, гдеFunc
— локальная функция. ЕслиMap
будет располагаться в другой единице трансляции (как в Рефале-05 + фреймворк), то косвенные функции придётся определять как entry. Преимущество несущественное. Для совместимости с фреймворком такие функции всё равно нужно определять как entry. А если почему-то хочется косвенную функцию разрешать по имени (например, для вызова<Map s.Func …>
, гдеs.Func
приходит извне), то можно использовать конструкцию(&Mu s.Func)
. - ❌ Включаемые файлы замедляют компиляцию программ — для каждого файла, включающего
$INCLUDE "LibraryEx";
, приходится заново сканировать и разбирать определения одних и тех же функций. - ❌ Цель, ради которой добавлялись в язык
$INCLUDE
, уже решается опцией-OG
. Более того, решаемая проблема эта сугубо второстепенная и техническая (ускорение конкретной реализации), но решалась она внесением нового средства в язык. - ❌ Реализация подключаемых файлов довольно громоздкая и некрасивая, слишком длинные и ажурные образцы. Пути рефакторинга мне не очевидны.
- ❌ В компилятор было бы неплохо добавить предупреждения о неиспользуемых функциях и неиспользуемых extern’ах (#326). Но одновременная поддержка включения файлов сильно затрудняет эту диагностику: во включённых файлах могут быть и неиспользуемые функции, и неиспользуемые extern’ы, о которых предупреждать не стоит.
- ❌ Использование
$INCLUDE
для оптимизации вызовов требует синтаксиса оторванных$ENTRY
(см. далее). - ❌ Когда функции библиотеки
LibraryEx
располагались в префиксе, префикс можно было собрать один раз с максимальной оптимизацией, после чего разработка компилятора незначительно ускорялась. Функции типаMap
вызываются повсеместно, поэтому их сборка с-OPR
заметно ускоряла работу. С переходом на$INCLUDE "LibraryEx";
это перестало работать. - ❌ Анализ
$INCLUDE
также несколько усложняет SRMake, в частности, в нём приходится анализировать escape-последовательности.
Таким образом, средство хоть и удобное, но избыточное, его удаление только упростит язык.
Синтаксис оторванных $ENTRY
(detached $ENTRY
)
Эта конструкция была добавлена в язык в сочетании с предыдущей — ради оптимизации некоторых функций библиотеки с сохранением возможности их вызова через обычный $EXTERN
. Подробнее смотри в #159.
- ✔ В сочетании с включением файлов позволяет осуществлять оптимизацию библиотечных функций при раздельной компиляции. Но это преимущество не очевидно, см. аналогичный пункт в предыдущем разделе.
- ✔ По аналогии с синтаксисом оторванных
$ENTRY
можно добавить синтаксис «пришитых»$DRIVE
,$INLINE
и, в перспективе,$OPT
. Про это даже задача есть — #250. - ❌ Очевидно, усложняется синтаксис языка. Когда пометка отделена от самой функции, появляется возможность синтаксической ошибки — функцию не определить, но пометить как entry. Т.е. синтаксис концептуально приводит к ошибкам.
- ❌ Очевидно, усложняется компилятор. На уровне синтаксического анализа ненамного, сложности вылезают на последующих проходах. В частности, именно в этой логике недавно была обнаружена (и исправлена) проблема #302.
Единственная причина, заставляющая этот синтаксис существовать в компиляторе — возможность оптимизировать map-подобные функции с раздельной компиляцией.
$SCOPEID
(#284)
Устаревшая конструкция языка. Подробнее — в задаче на удаление #284. Дублировать или конспектировать содержимое #284 я не буду.
Нативные вставки
Нативные вставки, они же вставки кода на С++, позволяют в исходном коде на Рефале-5λ описывать функции, тело которых реализовано на C++. По своему замыслу они являются аналогом ассемблерных вставок уже в самом C++ (или в расширениях некоторых компиляторов Си). В компилятор были добавлены в рамках задачи #11.
Мотивация их добавления была в возможности смешивать в одном файле код на Рефале и код на C++. В частности, так сделано для того, чтобы в одном файле Library.sref
иметь некоторые функции, написанные на Рефале, и некоторые функции, написанные на C++.
Рассмотрим их преимущества и недостатки.
- ✔ Возможность в одном файле иметь и функции, написанные на Рефале, и функции, описанные на C++. В частности, функция, написанная на C++ может быть локальной или даже безымянной: https://github.com/bmstu-iu9/refal-5-lambda/blob/b40f74f05126a2232ad113df040de5748f4059f9/src/lib/Library.ref#L510-L515
- ✔ Функции, написанные на C++, меньше зависят от деталей кодогенерации. Например, и в Простом Рефале, и в Рефале-05 однажды происходил переход от представления функций как пар 〈указатель на код, указатель на строку〉 к указателю на дескриптор, который сам содержит указатель на код и указатель на строку. Оба перехода были совершены после реализации нативных вставок и потребовали минимум изменений:
- https://github.com/bmstu-iu9/refal-5-lambda/commit/6f1a36670f903443577f37e9c27262e76cf1341b
- https://github.com/Mazdaywik/Refal-05/commit/5dc0c8e65b79c476dcc9bd2fb5c29c94b6c8d0a2
- ❌ Проблему в предыдущем пункте можно решить более грамотным продумыванием FFI. Например, в Рефале-05 для объявления и определения функций используются макросы, а значит, ту же реализацию дескрипторов можно менять, не требуя стереотипных (boilerplate) правок.
- ❌ Польза от перемешивания кода на Рефале и кода на C++ не очевидна. В актуальной реализации такое перемешивание активно используется при реализации длинной арифметики. Но длинная арифметика медленная (как раз из-за своей реализации), и её всё равно надо переписывать на C/C++. Но вот недостаток у перемешивания кода Рефала и C++ есть. Со смешанным файлом нельзя работать инструментами, ориентированными на C++, например, IDE с автодополнением.
- ❌ Возможность перемешивать код уже не совсем адекватна реализации компилятора. Простой Рефал изначально компилировался в C++ — так было задумано изначально. Поэтому использовать вставки кода для него была логичной. Рефал-5λ может компилироваться как в C++, так и в RASL, причём последний используется по умолчанию. Вставки кода на C++ при компиляции в RASL бессмысленны.
- ❌ Возможность компиляции с нативными вставками, т.е. написания на C++ локальной функции, требует такой костыльной вещи, как идентификаторы области видимости (#284). Все функции считаются как бы глобальными, но частью имени являются два целых числа — два нуля для entry-функций и два как бы случайных числа для локальных.
- ❌ Ну, и очевидно, синтаксис языка усложняется на одну конструкцию — нативные вставки.
- ❌ Усложняются преобразования программ. Например, нельзя удалить локальную функцию, если она нигде не вызывается из кода на Рефале. Она может вызываться из нативной вставки.
- ❌ Против нативных вставок также то, что они сначала были добавлены в Рефал-05 (https://github.com/Mazdaywik/Refal-05/issues/11), а затем после некоторого анализа всё-таки удалены из него (https://github.com/Mazdaywik/Refal-05/issues/36).
Подытоживая. Нативные вставки — не самый лучший способ осуществления FFI. При этом они требуют заметного усложнения архитектуры компилятора. Так что нужно продумывать хороший FFI и нативные вставки удалять из языка.
Директива $LABEL
Она нужна только в рамках актуального FFI — с нативными вставками. При использовании другого FFI она становится избыточной.
Простой Рефал (#327)
А вообще, зачем нужна поддержка этого front-end’а? Единственный эксплуатируемый код на Простом Рефале — это его самоприменимый front-end. Потому что если бы я перевёл front-end на Рефал-5λ, то никакой пользы от Простого Рефала уже не было бы.
Помимо front-end’а на Простом Рефале написана куча автоматизированных тестов. Небольшая часть тестов проверяют front-end Простого Рефала, остальные более глубокую логику. При удалении Простого Рефала первую часть тестов логично удалить, а вторую переписать на Рефал-5λ — нет никаких причин продолжать использовать Простой Рефал.
Есть задача по унификации парсеров Рефала-5λ и Простого Рефала — #201. Поскольку их синтаксисы похожи и различаются они по сути только идентификаторами и указателями на функции, можно обойтись одним парсером со внутренним флагом режима. В парсере Рефала-5λ уже есть флаг режима, его можно расширить. Но, если эту задачу реализовать, то объём эксплуатируемого кода на Простом Рефале уполовинится — останется только лексер.
Модульный Рефал поддерживает компиляцию себя в Простой Рефал. Но это не существенно, т.к. переделать кодогенератор на Рефал-5λ нет никакой сложности.
LexGen
Это генератор лексических анализаторов в виде конечного автомата. Также к нему частично написан front-end в виде синтаксиса регулярных выражений (#50).
На данный момент он используется только во front-end’е Простого Рефала. Если последний будет удалён, то генератор станет не нужен.
При разработке лексера Рефала-5λ LexGen не использовался по двум причинам. Во-первых, уже был готовый лексер Рефала-5 из тогда ещё будущего фреймворка. Во-вторых, написать хороший эффективный лексер не сложнее, чем написать автомат и постобработку.
В своём текущем виде генератор мало полезен. Написание автомата с его помощью не на много проще написания генератора лексических анализаторов вручную. Поэтому либо его надо дорабатывать, чтобы он наоборот облегчал разработку, либо выбрасывать.
Генерируемый код тоже не слишком эффективен. Вопросы вызывает эффективность построенных функций, порой содержащих десятки предложений. Не исключено, что вызов функции Type
и анализ её возвращаемого значения будет быстрее.
На доработку front-end’а уже давно есть задача #50. На оптимизацию генерируемого кода задача пока не ставилась.
Рассахаривание условий (-OC-
)
Режим рассахаривания условий существует в компиляторе по историческим причинам. Когда-то был Простой Рефал, в котором условий не было. Потом независимо я создал проект по конвертации Рефала-5 с условиями и блоками в его базисное подмножество.
Затем я решил внедрить условия в компилятор. На первом этапе я решил адаптировать конвертор, чтобы во входном языке были доступны условия, но при этом не требовалось менять back-end и рантайм.
Затем были реализованы условия (@Anastasya34, #17, #161), но режим рассахаривания условий остался. Режим нативной поддержки условий был оформлен как оптимизация -OC
(по умолчанию устанавливается скриптами rlc
и rlmake
), т.е. при использовании этого ключа рассахаривание условий отключается. Забавно, что это единственный режим оптимизации, включение которого ускоряет работу компилятора.
Рассахаривание условий был оставлено по двум причинам. Во-первых, оно позволило @Anastasya34 сделать сравнительные замеры производительности между нативной поддержкой условий и рассахариванием. Во-вторых, жалко было удалять.
Сейчас рассахаривание условий интересно тем, что в этом режиме возможны и прогонки условий (#248) и прогонки вызовов в условиях (#283). Это может быть актуально для классического Рефала-5 и некоторых стилей программирования. В частности, с рассахариванием условий и пометкой одной из функций как $DRIVE
можно ускорить MSCP-A (версия от января 2020) в два раза (там есть специфическое узкое место и оно в условии).
Но концептуально этот режим уже не нужен.
Избыточные ^
(#337)
Стоит запретить знак ^
, записанный после ранее не встречавшейся переменной.
Формально это не удаление синтаксического средства. Но, поскольку это сужение входного языка, то здесь оно вполне уместно.
Выводы
Перечислено несколько устаревших средств. Их нужно или удалить (те, которые удалить надо), или аргументированно оставить (те, с которыми всё неоднозначно).
- [ ]
$DRIVE
,$INLINE
,$SPEC
,-OD
,-OI
,-OS
— замена на$OPT
и-OT
(#314) - [ ]
$INCLUDE
- [ ] Синтаксис оторванных
$ENTRY
(detached$ENTRY
) - [ ]
$SCOPEID
(#284) - [ ] Нативные вставки — заменить их на нормальный FFI.
- [ ] Директива
$LABEL
- [ ] Простой Рефал (#327) — обдумать удаление (написать в комментариях).
- [ ] LexGen — обдумать удаление (написать в комментариях).
- [ ] Рассахаривание условий (
-OC-
) — обдумать удаление (написать в комментариях). - [ ] Избыточные
^
(#337) - [ ] Псевдокомментарий
*$EXTENDED
, старое поведение*$CLASSIC
(#195). - [ ] Классический и расширенный режимы должны стать обёртками над предупреждением
-Wclassic
(https://github.com/bmstu-iu9/refal-5-lambda/issues/318#issuecomment-905522573)
В заявке #337 предложена следующая стратегия удаления устаревших элементов:
Предлагается такая стратегия: версию, помеченную тегом N, всегда можно собрать версиями N−1 (техническое требование, ибо раскрутка) и N+1. Последнее требование новое, оно теперь будет определять политику подобных изменений. При этом, если некоторый синтаксис планируется объявить ошибкой со следующей версии, то в текущей версии должно выдаваться на это предупреждение.
Соответствующие предупреждения будут называться
-Wdeprecated
.
Классический режим как режим
Обзор
Компилятор поддерживает возможность компиляции как в режиме «расширенного» Рефала-5λ, так и в «классическом подмножестве». В «расширенном режиме» доступны все средства языка, в «классическом» — только те синтаксические конструкции, которые поддерживают refc
и crefal
. Использование «расширенного» синтаксиса в классическом режиме является синтаксической ошибкой.
Предложение было внесено в #144 без какой-либо мотивации.
В комментарии https://github.com/bmstu-iu9/refal-5-lambda/issues/195#issuecomment-890893270 предлагается отказаться от псевдокомментария *$EXTENDED
, псевдокомментарий *$CLASSIC
— сделать глобальным (единственное его упоминание в любом месте исходника включает классический режим на весь файл).
Из-за того, что в актуальной реализации используется переключение режимов псевдокомментариями, по всему исходнику парсера приходится таскать флаг текущего режима (который может меняться). При изменении семантики *$CLASSIC
и удалении *$EXTENDED
флаг останется, но будет неизменным (в выходных форматах функций его можно будет убрать).
Как переделать
Когда включен «классический» режим, компилятор считает некоторые синтаксические конструкции ошибками. Когда он выключен, не считает. В компиляторе уже есть другой механизм, способный в зависимости от настроек или считать, или не считать некоторые синтаксические/семантические конструкции ошибками, прерывающими компиляцию. Предлагается «классический» режим реализовать через него.
Речь идёт о предупреждениях и режиме -Werror=…
. Если предупреждение выключено, программа принимается молча. Если включено — выдаются сообщения, не препятствующие компиляции. Если включен режим -Werror=…
, то предупреждения считаются ошибками, прерывающими компиляцию.
При условии реализации https://github.com/bmstu-iu9/refal-5-lambda/issues/195#issuecomment-890893270 (т.е. удалении *$EXTENDED
и изменении семантики *$CLASSIC
) трактовку расширенных конструкций в классическом режиме можно изменить — трактовать их как предупреждения -Wclassic
:
- Расширенный режим будет соответствовать
-Wno-classic
. Предупреждения на расширенные конструкции не выдаются, это нормальный режим пользования Рефалом-5λ. - Классический режим будет соответствовать
-Werror=classic
. Конструкции Рефала-5λ, отсутствующие в классическом Рефале-5, будут трактоваться как синтаксические ошибки. - Появится и промежуточный режим, включаемый
-Wclassic
(безerror
) — выдача предупреждений без прерывания компиляции. Такой режим может быть полезен, например, при миграции с Рефала-5λ на классический Рефал-5: по одному убирать предупреждения, после каждой правки тестируя код.
Опция --classic
будет неявно соответствовать -Werror=classic
, опция --extended
— -Wno-classic
. Псевдокомментарий *$CLASSIC
будет принудительно включать -Werror=classic
на конкретный файл. Получится функция, в чём-то противоположная подавлению предупреждений (#345).