Open Mazdaywik opened 2 years ago
Несколько дополнительных замечаний
$EXTERN
’амОбнаружен ещё один аспект несовместимости Рефала-5 и Рефала-05. Рассмотрим такую программу из единственного исходника:
$EXTERN NotCalled;
$ENTRY Go {
= <Prout NotCalled>
}
В исходном файле функция NotCalled
нигде не вызывается, поэтому Рефал-5 (и Рефал-5λ) объявление $EXTERN
для неё проигнорирует. Отсутствие entry-функции NotCalled
нигде в программе Рефал-5(λ) не смутит, программа успешно запустится и выполнится.
Но у Рефала-05 будут проблемы. Имя NotCalled
окажется нагруженным идентификатором — неявно привяжется к $EXTERN
’у. Соответственно, при попытке собрать мы увидим соответствующую ошибку компоновщика языка Си.
Считать это проблемой, требующей решения, мы не будем, будем считать особенностью Рефала-05. Рефал-05 может быть у́же, чем Рефал-5, в частности, не компилировать некоторые корректные программы Рефала-5 — см. следующий раздел.
$EXTERN
'ыАктуальная версия Рефала-05 достаточно строга — она выдаёт ошибки там, где компилятор Рефала-5 должен либо молчать, либо выдавать предупреждения. Это ошибки:
$EXTERN
’а, когда в одном файле имеется и объявление, и определение функции, либо два объявления,Первая ошибка нас сейчас не интересует, интересует вторая.
Проверка на неиспользуемые функции была добавлена из-за того, что компилятор BCC 5.5.1 выдавал предупреждения на неиспользуемые статические функции, а мне хотелось, чтобы компиляция back end’а всегда проходила гладко. Таким образом, я решил явно запретить во входном файле неиспользуемые локальные функции — это полезно и с точки зрения стиля тоже.
Проверка на неиспользуемые функции была добавлена коммитами cc65ec6b166dbab4c4a6befea2924037b957e435 и 10a94ba51fb5d6b15a875fde07d4bd7241d8fcd6, причём, как следует из сообщения первого коммита:
Во-вторых, сообщает о неиспользуемых $EXTERN’ах. Зачем? А почему бы и нет?
проверка на неиспользуемые объявления была добавлена совершенно произвольно. Технической потребности (в отличие от неиспользуемой функции) у неё не было, она была добавлена из стилистических соображений и простоты реализации (легко реализовать «заодно»).
Проблема может быть в том, что программист, привыкший к Рефалу-5, может удивиться, что на одни $EXTERN
’ы компилятор ругается (они действительно никак не используются), а на другие — нет, хотя соответствующие функции нигде не вызываются (но есть одноимённые идентификаторы).
✔ Проблемой это также не является, т.к. язык другой, в нём другие правила, хотя синтаксис и похож.
ЕМНИП, в Рефале-2 используется точно такая же семантика для символов-имён — они сравниваются по именам, но при этом несут с собой указатель на функцию, видимую из точки их создания.
Отличие состоит в том, что в Рефале-2 пустые функции должны создаваться при помощи EMPTY
, в предлагаемой редакции Рефала-05 это делать не требуется (компилятор сам определит пустые функции).
Но вообще, надо сверить с документацией и реализацией Рефала-2, память может подводить.
Всё-таки делать буду. Так от Рефала-05 будет больше пользы.
Ещё несколько дополнительных замечаний.
$EXTERN
’ам и предупрежденияВ комментарии выше (https://github.com/Mazdaywik/Refal-05/issues/38#issuecomment-1250591131) я рассуждал на тему того, что у нас могут быть внешние объявления, которые избыточны в Рефале-5 (т.к. такие функции не вызываются), но при этом учитываются в Рефале-05 (т.к. упоминаются имена-функции-идентификаторы).
Это может удивлять не только программистов. Если для Рефала-5 будут реализованы диагностические инструменты, сообщающие о неиспользуемых $EXTERN
’ах (например, предупреждения в Рефале-5λ или во фреймворке), то на такие $EXTERN
’ы они тоже будут выдавать диагностику.
Сначала я подумал, что для этого стоит оставить псевдокомментарий *$EXTERN
, который и будет использоваться для такого рода ссылок на имена, но потом отказался от этой идеи. Можно даже требовать, чтобы ссылки на вызываемые функции были в директиве $EXTERN
, а ссылки на идентификаторы — в псевдокомментарии. (т.е. выдавать синтаксическую ошибку на обратное). Достоинство у неё только одно:
Недостатков больше, причём один из них существенный.
$EXTERN
и *$EXTERN
— серьёзное и неоправданное. По сути предлагается ведь использовать псевдокомментарий ради совместимости с предупреждениями в других инструментах, которые даже не реализованы. А поскольку псевдокомментарий остаётся только один (*$ENUM
’ы в топку), то это просто сохранение поддержки псевдокомментариев ради непонятно чего.$EXTERN
ради создания нагруженного идентификатора, на мой взгляд, будет редка. Сейчас $EXTERN
’ы ради имён пишутся только для пустых функции, вроде имён токенов и прочих True
, False
, Success
, Fails
… Но если идентификаторы будут сравниваться по их строковому представлению, то такие $EXTERN
’ы будут не нужны. Единственный сценарий — делаем ссылку на внешнее имя, чтобы вызвать его потом через Mu
. Это слишком редкий сценарий и мне трудно представить ситуацию, когда он будет оправдан.Таким образом, псевдокомментарии для *$EXTERN
не нужны.
*$ENUM
’ы и *$EENUM
’ы не нужны
Пустые функции нельзя вызывать в том файле, где они определены — front-end из фреймворка их не видит и на вызов функции выдаст ошибку. Можно вызвать их из другого файла, сославшись на них через $EXTERN
, но такая программа не входит в общее подмножество — Рефал-5 по умолчанию её не загрузит (без опции -i
получим ошибку о неопределённой внешней ссылке). А использовать внешний файл с пустой функцией — явный костыль. К тому же, есть переносимый способ создать функцию с пустой областью определения:
Fail { e.X, A : B = }
Вызвать их нельзя, как имена они уже не нужны, значит, они не нужны.
*`$EXTERN`’ы тоже не нужны** Полное обоснование — в разделе выше.
Так что из соображений минимализма удаляем долой всю поддержку псевдокомментариев.
Выше я написал:
ЕМНИП, в Рефале-2 используется точно такая же семантика для символов-имён — они сравниваются по именам, но при этом несут с собой указатель на функцию, видимую из точки их создания.
Я был неправ. Рассмотрим такую программу из трёх модулей:
(В компиляторе какой-то глюк со вложенными угловыми скобками, поэтому здесь мешанина < … >
и k … .
.)
Здесь в модулях mod1
и mod2
имеется по локальной функции с именем Local
, entry-функции в них возвращают составной символ — имя этой функции. Функция GO
распечатывает их и сравнивает на равенство при помощи Compare
. Вывод программы говорит сам за себя:
/LOCAL/'mod1'
/LOCAL/'mod2'
'Equal'/LOCAL/('mod1')
'Not equal'/LOCAL/('mod1')/LOCAL/('mod2')
Concretization is executed
Total steps number = 20
Так что семантика Простого Рефала до внедрения # Меток
и актуальная семантика Рефала-05 идентичны семантике Рефала-2.
Кстати, в документации нигде не заостряется внимание на семантике равенства составных символов, для авторов руководства это, по-видимому, считается самим собой разумеющимся.
Mu
Mu
Выше предлагалось:
Расширение семантики
Mu
Семантику
Mu
можно приблизить к семантике Рефала-5 — генерировать таблицу из функций текущей единицы трансляции. В этом случаеMu
сначала ищет функцию в таблице без учёта нагрузки, а затем, если не нашлось, вызывает нагрузку.
Не нужно, т.к. это противоречит минимализму.
Mu
в программах будет вызываться не всегда. Если генерировать функцию Mu
всегда, то мы получим неиспользуемую статическую функцию, на которую может выдавать предупреждения компилятор Си. Если генерировать, когда она вызывается, то мы опять же, усложним компилятор.Так что не надо её генерировать.
Mu
уже описана! Турчиным!Она описана в учебнике, в разделе 6.1. Скопирую текст из русского перевода учебника:
Таким образом, можно говорить, что предлагаемая семантика функции Mu
, является «Mu
-Динамической (MuD
)», как она описана Турчиным!
Ещё соображение.
В Рефале-5 составные символы могут быть записаны как в идентификаторной форме — имя, начинающееся с буквы (или прочерка — недокументировано) и состоящее из букв, цифр, прочерков и минусов, так и в виде составного символа в двойных кавычках. В последнем допустимы вообще любые символы, т.к. можно использовать escape-последовательности.
Актуальная реализация такие составные символы поддерживает, и, если они могут быть записаны в идентификаторной форме и одноимённая функция есть в области видимости, программа скомпилируется:
$ENTRY Go {
= "Go" "\x47\x6f" /* Go * в hex-кодах */
}
Если в исходной программе будет записан идентификатор в «закавыченной» форме, не представимый в идентификаторной, то для него одноимённой функции заведомо не найдётся — имя функции должно быть в идентификаторной форме.
Реализация настящей задачи предполагает автоматическую генерацию пустых функций для идентификаторов.
Проблема здесь в том, что идентификаторы в «закавыченной» форме нужно будет как-то представлять в исходном тексте программы. Варианты:
":-)"
→ ident_k58_m_k41_
.
q1_
, q2_
, q3_
, …
МОИДОК~1
, PROGRA~1
и т.д.) фактически является замороченной разновидностью второго варианта.Промежуточный вывод: нужно проверить, приводят ли слишком длинные идентификаторы к проблемам с компилятором Си (например, он в длинных именах может учитывать только первые 64 знака) и, если приводят, выбрать вариант с переименованием.
Был сделан замер производительности в двух вариантах: сравнение по указателю или с использованием strcmp()
. Для выбора варианта использовалась условная компиляция:
В обоих случаях есть накладные затраты на вызов функции equal_functions()
, поэтому различия в производительности компилятора демонстрируют только разницу между сравнением указателей и сравнением имён.
Использовался компилятор Borland C++ Compiler 5.5.1:
D:\Mazdaywik\Documents\Refal-05\src>bcc32
Borland C++ 5.5.1 for Win32 Copyright (c) 1993, 2000 Borland
Компиляция осуществлялась без какой-либо оптимизации:
D:\Mazdaywik\Documents\Refal-05\src>set R05CCOMP
R05CCOMP=bcc32 -w
Было выполнено по 15 прогонов (надо было 13 или 17, но перепрогонять уже было неохота) для #if 0
и #if 1
:
В качестве медианы брался 8-й замер (очевидно, 15 // 2 == 8
), в качестве доверительного интервала — 4-й и 12-й (т.е. 4-й с конца). Это несколько шире, чем квартили.
Общее время работы:
strcmp()
: 12,182 [12,098…12,261] секунд, замедление на 0,7 %, условно достоверное (медиана для указателей не попадает в доверительный интервал для strcmp()
).Чистое время сопоставления с образцом:
strcmp()
: 1,355 [1,260…1,569] секунд, замедление на 1,0 %, недостоверное,Время сопоставления с повторными e-переменными в циклах по открытым e-переменным:
strcmp()
: 0,110 [0,077…0,125] секунд, доверительные интервалы совпадают, погрешности близки к величине квантования.Вывод. Достоверного замедления не обнаружено, а то, что выявлено — влияет на производительность ничтожно (0,7 %). Дополнительных оптимизаций производить не требуется.
Промежуточный вывод: нужно проверить, приводят ли слишком длинные идентификаторы к проблемам с компилятором Си (например, он в длинных именах может учитывать только первые 64 знака) и, если приводят, выбрать вариант с переименованием.
В стандарте языка Си есть требования к длине имени. Из черновика C99:
— 31 significant initial characters in an external identifier (each universal character name specifying a short identifier of 0000FFFF or less is considered 6 characters, each universal character name specifying a short identifier of 00010000 or more is considered 10 characters, and each extended source character is considered the same number of characters as the corresponding universal character name, if any. — 4095 external identifiers in one translation unit
В N2176.pdf (2017-й год) требование не изменилось.
Я реализовал схему с декорированием, но если это когда-нибудь приведёт к проблемам, буду думать, что делать дальше.
Если я правильно понимаю стандарт, мне больше актуален этот абзац:
63 significant initial characters in an internal identifier or a macro name (each universal character name or extended source character is considered a single character)
Для идентификаторов неявно определяются пустые функции в целевом файле с модификатором static
. Если я правильно понимаю стандарт (лень сейчас вникать), то это не внешнее имя.
Имя макроса (macro name) — это про стадию препроцессора, задаётся директивой #define.
static — идентификаторы с т.н. internal linkage, в стандарте количество значащих символов было определено только для старого ANSI C как 31 (есть по ссылке на MSVC). Стандарт не разделяет транслятор и линкер, но по сути требование выставляет к последнему (внешние символы приходится сохранять в объектном файле, а при внутреннем связывании для любой длины можно хранить хеш).
Из документации по популярным трансляторам:
MSVC: the Microsoft C compiler allows 247 characters in an internal or external identifier name.
GCC: For internal names, all characters are significant. For external names, the number of significant characters are defined by the linker; for almost all targets, all characters are significant.
Microsoft пишет о допустимом количестве символов, а GNU — о значащих, как в стандарте. Для последнего ограничений в количестве не вижу, для препроцессора заявляют, что лишь бы уместилось в память.
Для Clang не нашёл, но они стремятся к совместимости с GCC. Зато попался Sun WorkShop Compiler C 5.0: The first 1,023 characters are significant.
Похоже, если проблемы и возможны, то с очень старыми либо слишком экономными трансляторами.
Исследовал мой любимый компилятор BCC 5.5.1 2000-го года выпуска:
int a123456789b123456789c123456789d123456789e123456789f123456789g123456789h123456789i123456789j123456789k123456789l123456789m123456789n123456789o123456789p123456789q123456789r123456789s123456789t123456789u123456789v123456789w123456789x123456789y123456789z123456789;
В идентификаторе в исходном файле 26 × 10 = 260 знаков. В объектном файле только 250 — кусочек z123456789
срезался:
Проблема
На актуальной реализации Рефала-05 программировать неудобно и часто непросто переносить другие программы (см. https://github.com/Mazdaywik/Refal-05/issues/28). Причина — объявления
*$ENUM
’ов и*$EENUM
’ов.Причина появления пустых функций — (подразумеваемая) цель разработки, как она была описана в https://github.com/Mazdaywik/Refal-05/issues/33:
В Простом Рефале пустые функции были (по аналогии с Рефалом-2). Пустые функции допускали простую и эффективную реализацию символических имён при компиляции в язык Си (для сравнения — идентификаторы вида
# Имя
не выжили, т.к. эффективной реализации в Си не было).Как и в Простом Рефале и в Рефале-5λ, такая реализация допускала библиотечные функции высшего порядка (например,
Map
вLibraryEx
), это тоже важный для меня критерий. Если функцииMap
не будет, то мне программировать на Рефале будет грустно.Простой Рефал предполагался как исследовательский проект — я его делал для себя, чтобы лучше понять Рефал и то, как его компилировать. А для исследовательского прототипа некоторые недостатки входного языка несущественны и вполне терпимы, цель у него другая.
Рефал-05 является точным синтаксическим подмножеством Рефала-5 и почти во всём он совместим с ним семантически. Ряд программ для Рефала-5 (при реализации соответствующих встроенных функций, см. #28), вполне может быть запущен и Рефалом-05, для этого требуется лишь определить пустые функции для используемых идентификаторов.
Требуется лишь определить. На практике оказалось, что для MSCP-A потребовалось определить несколько десятков идентификаторов, я замучался их определять и так и не довёл дело до конца. SCP4 собирать Рефалом-05 я даже не пробовал. Кроме того, в программах на Рефале-5 часто используются функции
Implode
/Implode_Ext
, мне эти функции не нравятся, но факт остаётся фактом. Чтобы запустить некоторые программы, их использующие, в #28 мне пришлось сооружать костыли.Ради совместимости с Рефалом-05 мне пришлось фреймворк refal-5-framework засорить всеми этими
*$EENUM
’ами, что, в свете вышеизложенного, уродливо.Предлагаемое решение
Предлагается вместо функций в роли имён использовать идентификаторы, сравниваемые по текстовому представлению. Их объявлять явным образом не надо (псевдокомментарии
*$(E)NUM
становятся комментариями и уходят в прошлое), одинаковые имена, записанные в разных единицах трансляции, являются равными.Примитивнейший способ проверки на равенство —
strcmp()
, но тут возможны оптимизации (хотя интересно сделать замер и с однимstrcmp()
тоже).А как же тогда быть с функциями высшего порядка? А предлагается вместе с идентификатором нести невидимую полезную нагрузку — указатель на одноимённую функцию, видимую из точки построения идентификатора. Пример:
Полезная нагрузка не участвует в сравнении на равенство при сопоставлении с образцом, потому и названа невидимой.
Функция
Mu
смотрит на полезную нагрузку и вызывает соответствующую функцию. В ненагруженные идентификаторы можно класть либоNULL
, либо функцию, которая для любого аргумента выдаёт ошибку невозможности отождествления.На нагруженный идентификатор — кортеж из имени и опционального указателя — можно посмотреть и с другой стороны, буквально. Фактически, здесь предлагается для неопределённых имён функций (в позициях, кроме после
<
) неявно генерировать*$ENUM
и сравнивать символы-функции не по ссылкам на дескрипторы, а по именам. То есть, рантайм существенно не изменится (изменится только проверка на равенство для одного из типов символов), компилятор вместо выдачи ошибок будет генерировать недостающие пустые функции.Историческое замечание
Подобная идея была предложена Скоробогатовым, он мне сообщил об этом в личной беседе много лет назад, когда он сам ещё разрабатывал Рефал-7 (в конце 2000-х — в начале 2010-х). В Рефале-7 предлагались именованные вложенные функции. Я спросил:
Диалог не дословный, но суть передал насколько помню. Данный подход мне не понравился — в те годы Скоробогатов также планировал реализовать специализатор для Рефала-7, а тут с аксиоматикой равенства возникали существенные проблемы (например, что копия значения не равна себе). Вообще, с функциями высшего порядка, их равенством и глубокими трансформациями программ есть много сложностей — см., например, https://github.com/bmstu-iu9/refal-5-lambda/issues/276.
Но здесь описанные проблемы несущественны — цель обеспечить совместимость с Рефалом-5 в практически значимых случаях, а глубокие преобразования для программ на Рефале-05 не планируются.
Возможные оптимизации
Постоянно дёргать
strcmp()
для проверки на равенство может быть неэффективно (но надо померять!). Поэтому можно предложить следующие оптимизации.Implode
), во втором — потеря быстродействия. К тому же не все хэш-функции удобно описывать на Рефале. Если два хэша не равны, значит, заведомо не равны и сами символы. Если равны — надо сравнивать на точное равенство.strcpy()
регулярно будет проверяться только если в программе более 2¹⁶ идентификаторов.Расширение семантики
Mu
Семантику
Mu
можно приблизить к семантике Рефала-5 — генерировать таблицу из функций текущей единицы трансляции. В этом случаеMu
сначала ищет функцию в таблице без учёта нагрузки, а затем, если не нашлось, вызывает нагрузку.UPD: не нужно, https://github.com/Mazdaywik/Refal-05/issues/38#issuecomment-1253225207.
Пример на несовместимость с Рефалом-5
Не смотря на то, что предложенная семантика стала ещё ближе к Рефалу-5, несовместимость остаётся всё равно. Пусть
s.Func
содержит имя некоторой entry-функции программы. Тогда<Mu s.Func …>
в Рефале-5 вызовет эту функцию, если одноимённого имени не было в точке вызова, а в Рефале-05 — если при создании этого имени в области видимости было либо определение этой функции с$ENTRY
, либо объявление с$EXTERN
.Обе функции
Get1
иGet2
вернут нагруженные идентификаторыA
иB
, однако толькоGet1
вернётC
как нагруженный.Get2
вернёт пустой.Эта программа на Рефале-5 напечатает
В Рефале-05 напечатает
и аварийно упадёт из-за вызова ненагруженного идентификатора. Если же расширить
Mu
, как предложено выше, то получимПреимущества
*$EENUM
, тут*$EXTERN
).Implode
/Implode_Ext
— просто создаются идентификаторы без нагрузки. Кроме, возможно, имён, совпадающих со встроенными функциями.Недостатки
Здесь в первом предложении могут быть вызваны разные одноимённые функции. На сколько это может привести к проблемам, мне не очевидно.
Альтернативный путь
Чтобы обеспечить семантику
Mu
Рефала-5 с идентификаторами, можно пожертвовать раздельной трансляцией, как это предложено в https://github.com/bmstu-iu9/refal-5-lambda/issues/324 и https://github.com/bmstu-iu9/refal-5-lambda/issues/197. Однако, это выглядит слишком громоздким для Рефала-05.Этапы
strcmp()
, оценить быстродействие.Оптимизировать его, если это необходимо.$EXTERN
’ы для имён.