Рефал-05 — подмножество Рефала-5 и альтернативная реализация Рефала-5, компилирующаяся в C89.
Язык Рефал-05 является точным подмножеством Рефала-5, т.е. любая программа, которая корректно работает на Рефале-05, будет точно также работать и на Рефале-5, обратное неверно.
Ставились следующие цели разработки:
В 2007 году автор задумал написать свой диалект Рефала, сочетающий модульность из Оберона и возможность расширения набора встроенных функций — Модульный Рефал. Первая версия Модульного Рефала компилировалась в Рефал-5 PZ, т.к. автор не знал, как преобразовывать Рефал в императивный код.
В 2009 году автор самостоятельно придумал, как компилировать код на Рефале в C++, не изучая дополнительную литературу. Подход отличался от подхода, предложенного С.А. Романенко в своей диссертации: у С.А. Романенко запоминались позиции всех сопоставленных элементов, у автора — отслеживались границы дырок и e-переменные как закрытые интервалы (для t-переменных запоминался указатель на начало). Был написан компилятор из Рефала в C++, входной язык назывался Простой Рефал, больше всего напоминал Рефал-5, но не был совместим с ним.
Затем Простой Рефал стал использоваться как стадия синтеза для Модульного Рефала: последний может порождать на выходе как исходники на Простом Рефале, так и код на C++, совместимый с рантаймом Простого Рефала.
Параллельно, автор, работая в университете, стал совместно со студентами проводить исследования над Простым Рефалом, в частности реализовывать новые синтаксические конструкции и добавлять разного рода оптимизации.
У Простого Рефала был серьёзный недостаток: для нового языка нет программ (кроме своих исходников) и потому он бесполезен. Компилятор Простого Рефала в последствии был переделан в компилятор Рефала, являющегося надмножеством Рефала-5 — в Рефал-5λ. Он может компилировать такие программы, как суперкомпиляторы SCP4 и MSCP-A.
Рефал-5λ силами студентов рос и распухал — в рамках курсовых и ВКР в него добавлялись новые возможности, преимущественно, оптимизации. И он перестал быть таким простым, каким был изначально Простой Рефал. Автору захотелось иметь принципиально минималистичную реализацию Рефала.
Но, учитывая опыт превращения Простого Рефала в Рефал-5λ, минималистичная реализация должна быть полезной — совместимой с имеющимися программами других разработчиков. Поэтому был взят старый коммит Простого Рефала, упрощён (удалены избыточные возмжоности) и сделан совместимым с Рефалом-5. Рефал-5 был выбран по двум причинам: Простой Рефал больше всего на него похож и автор больше всего привык к Рефалу-5. Кроме того, совместимость со сторонней реализацией упрощает процесс раскрутки — не нужно хранить в репозитории папку полусобранных исходников, т.к. можно воспользоваться сторонней реализацией.
Примечание. Автор сначала хотел написать минималистичный «классический» Простой Рефал, очищенный от избыточных конструкций. Но потом передумал, потому что Простой Рефал не нужен, когда есть Рефал-5λ.
Минималистичная реализация была названа Рефал-05. Название напоминало «родственный» Рефал-5, «0,5» означало половинку, что это не полноценная реализация Рефала-5.
Язык был совместим с Рефалом-5 в том смысле, что существует общее подмножество обоих языков, довольно широкое, в частности, на нём сам Рефал-05 и написан. Однако, оба языка были несовместимы, особенно, в семантике символов-слов — в Рефале-05 они должны были быть именами функций из текущей области видимости (как в Рефале-2). Ради этого и ради совместимости с Рефалом-5 в синтаксис были добавлены костыли псевдокомментариев, которые позволяли объявлять пустые функции (тоже как в Рефале-2), Рефалом-5 они игнорировались.
Синтаксически поддерживалось только базисное подмножество Рефала-5, но с расширениями:
*$ENUM, *$EENUM, *$EXTERN,<s.Func e.Arg> — возможность
после угловой скобки не писать имя функции.В руководстве на тот момент было неплохое описание синтаксиса языка и длиннющее описание отличий Рефала-05 от Рефала-5 и того, как попасть в общее подмножество (можно поискать в старых коммитах). Наличие неплохого описания синтаксиса однажды даже привело к путанице между Рефалом-5 и Рефалом-05 — документация по Рефалу-05 была порекомендована как документация по Рефалу-5🤦♂️
Иметь язык, частично совместимый с более распространённым языком, лучще, чем несовместимый ни с чем. Но хуже, чем совместимый полностью либо являющийся точным подмножеством.
Рефал-05 в том виде также рассматривался как набор повторно используемых
компонент — предполагалось использовать его парсер, генератор кода
и библиотеку LibraryEx как библиотеки — первые для инструментальных
средств для работы с Рефалом-05, последнюю — как, собственно, библиотеку.
Однако, инструментальные средства для специфического языка не нужны.
В результате автор решил использовать как набор повторно используемых
средств ранее написанный проект по рассахариванию условий и блоков Рефала-5
до базисного подмножества, чей репозиторий
https://github.com/Mazdaywik/5-to-basis был переименован
в https://github.com/Mazdaywik/refal-5-framework, туда же уехала полезная
библиотека LibraryEx.
Парсер в Рефале-05 был заменён на парсер Рефала-5 из упомянутого фреймворка, из него же стал использоваться рассахариватель до базисного подмножества (генератор и рантайм поддерживают только базисный Рефал), а из документации было удалено описание синтаксиса за ненадобностью. Последнее существенно снизило вероятность путаницы между Рефалом-05 и Рефалом-5. Был взят курс на последовательное приближение Рефала-05 к Рефалу-5.
В истории коммитов можно заметить несколько тегов версий. Вот как они озаглавлены в https://github.com/Mazdaywik/Refal-05/milestones:
Версия 1.0 должна включать самоприменимый компилятор в новом синтаксисе. Библиотека должна быть реализована через нативные вставки. Поддержка C89 пока не обязательна. А вообще, веха создаётся для того, чтобы можно было выкладывать задачи в порядке их выполнения.
Компилятор Рефала-05 в C89, рантайм и библиотека написаны на C89.
Документированный готовый к практическому использованию компилятор
Рефал-05 — эффективный и при этом минималистичный, совместимый с Рефалом-5 язык, компилирующийся в Си с раздельной трансляцией.
Актуальная версия Рефала-05 совместима с версией Рефала-5 PZ Oct 29 2004
настолько, чтобы поддерживать сборку суперкомпиляторов SCP4
и MSCP-A и некоторые другие программы для Рефала-5.
Выбрана совместимость со старой версией PZ Oct 29 2004, поскольку на данный
момент публично доступные версии суперкомпиляторов написаны для неё, также
для совместимости с нею можно написать компактный компилятор с рантаймом.
Более новые версии, поддерживающие Юникод, потребуют или подключения толстой
сторонней библиотеки (требующей C99+, в то время как Рефал-05 использует C89),
или самодельной реализации Юникода, которая по объёму будет сопоставима
с текущим размером рантайма и стандартной библиотеки (не считая таблиц).
Ради минимализма и совместимости с Си используется раздельная трансляция: каждый исходник на Рефале независимо компилируется в исходник на Си. Однако, раздельная трансляция затрудняет реализацию метафункций.
Метафункции должны вызывать функцию по имени из того файла, где записан вызов метафункции, если же в текущей единице трансляции функции с этим именем не нашлось, осуществляется поиск среди всех entry-функций программы. Об этом не написано в учебнике Турчина, но есть в официальных дополнениях к книге. Этой «тайной» возможностью пользуется, например, SCP4.
Поиск в текущей области видимости просто решается таблицей, перечисляющей функции текущего файла — ищется функция, имеющая требуемое имя. А вот поиск по имени среди всех entry-функций оказывается затруднён. В качестве минималистичного, эффективного и переносимого решения были выбраны нагруженные идентификаторы, о чём подробно написано во второй части руководства.
Однако, нагруженные идентификаторы не обеспечивают полную совместимость с Рефалом-5 (детали см. в той же второй части). Альтернативные реализации, известные автору, должны будут жертвовать минимализмом, эффективностью или переносимостью — автор решил пожертвовать совместимостью с Рефалом-5. Если читатель знает альтернативный вариант, который ничем не жертвует — просьба написать автору или открыть заявку.
Совместимость с Рефалом-5 также обеспечивает возможность раскрутки компилятора — не требуется хранить или уже собранный исполнимый файл ОС для начальной раскрутки, или набор полускомпилированных исходников в Си (что увеличивает размер и усложняет организацию репозитория). Актуальную версию можно раскрутить при помощи Рефала-5 PZ/ПѮ или Рефала-5λ.
Рефал-5 версий PZ и ПѮ имеют различный размер макроцифры в зависимости
от используемого компилятора Си. Для совместимости Рефал-05 позволяет настроить
размер макроцифры макросами R05_NUMBER_‹тип› — подробнее в третьей части
руководства.
Рефал можно компилировать как в промежуточный интерпретируемый код, традиционно называемый RASL’ом (Refal assembly language), так и непосредственно в исходный код на другом языке программирования, например, Простой Рефал компилировался в C++.
Была выбрана компиляция в язык высокого уровня. Её преимущества:
Было решено выбрать в качестве целевого язык ANSI/ISO C89 по следующим причинам:
Для удобства разработки была принята (сохранена из Простого Рефала) раздельная трансляция: каждый исходный файл на Рефале транслируется в файл на Си, затем компилятору Си передаются как оттранслированные файлы, так и файлы библиотек, сразу написанные на Си (библиотеки поддержки времени выполнения и встроенных функций). Такой подход упрощает добавление собственных внешних функций на Си: достаточно написать исходник на Си, как описано в четвёртой части руководства, и добавить его в командную строку компилятора.
Рассмотрим проектные решения, вытекающие из этого требования.
Классическая списковая реализация — хорошо известна программистам на Рефале, не требует сложных алгоритмов работы с ней вроде подсчёта ссылок или сборки мусора.
Сопоставление с образцом сделано общепринятым способом, описанным ещё в диссертации С.А. Романенко — общепринятый способ хорошо известен и прост, сторонним программистам может быть проще в нём разобраться. Хотя, замеры показали, что после переписывания сопоставление с образцом стало работать медленнее, автор так и не разобрался, почему.
Символы-слова — указатели на функции. В отличие от Рефала-5 PZ/ПѮ, где указатель на код хранится в открывающей угловой скобке, поле зрения в Рефале-05 проще — не нужно хитрого хака для провязывания скобок активации в стек.
Поле информации угловых скобок хранит указатель, после открывающей скобки располагается символ-слово с указателем на описатель функции (содержит имя и указатель на функцию Си), угловые скобки провязаны в стек естественным образом, рефал-машина находит функцию как следующее звено после открывающей скобки.
Символы-слова как данные сравниваются на равенство по именам — просто
используется функция strcmp(), измерения показали, что она не является узким
местом, дополнительных оптимизаций здесь не требуется.
Если идентификатор является именем существующей функции, то он содержит ссылку на неё, если нет — компилятор создаёт пустую функцию, на которую идентификатор будет ссылаться.
Соответственно, при компиляции результатных выражений компилятор единообразно
компилирует идентификаторы как после «<», так и в других местах — просто
помещает ссылку на функцию.
Подход «символы-слова — указатели на функции» также обеспечивает почти полную реализацию метафункций. Напомним, что метафункции сначала ищут функцию по имени в точке своего вызова, а затем среди всех entry-функций программы. Первый шаг легко осуществить, создавая для каждого файла таблицу функций — метафункция ищет в этой таблице функцию по имени. При раздельной трансляции глобальную таблицу entry-функций во время компиляции в Си переносимо создать невозможно, а формирование такой таблицы во время выполнения противоречит целям простоты и эффективности. Проблема почти полностью решается тем, что имена хранят указатели — метафункция вызывает функцию по указателю, хранимому в символе. Однако, Рефал-05 должен быть точным подмножеством, поэтому каждый символ-слово хранит также булевский флаг — является ли он entry-функцией — если символ на entry-функцию не ссылается, метафункция откажется его вызывать.
Актуальная версия имеет единственную стороннюю зависимость: парсер Рефала-5,
написанный в рамках проекта https://github.com/Mazdaywik/refal-5-framework.
Сгенерированный код использует только стандартный язык Си89 без каких-либо
расширений, библиотечные компоненты тоже используют стандартные средства Си,
либо обращаются к стандартным средствам операционной системы, если стандарта
языка остаётся недостаточно. Функция XMLParse не реализована, и, вероятно,
не будет реализована никогда из этих соображений.
В актуальной версии отсутствует поддержка Юникода, отчасти, тоже из этих соображений: либо к проекту нужно добавлять стороннюю зависимость, либо реализовывать Юникод вручную. Второй подход, вероятно, будет громоздким, что не отвечает требованию минимализма. Но нельзя исключать, что Юникод всё-таки добавлен будет, возможно, не полностью.
В ранних версиях Рефала-05 поддерживался только базисный Рефал, тоже
из соображений минимализма. Однако, соображения совместимости с Рефалом-5
перевесили. Библиотека refal-5-framework, предоставляющая парсер
(и полезную библиотеку LibraryEx), предоставляет также и рассахариватель
условий и блоков до базисного подмножества. Рассахариватель позволил
использовать расширенные конструкции во входном языке без изменения стадии
синтеза компилятора — в актуальной версии она всё ещё поддерживает только
базисное подмножество.
То, что язык является подмножеством Рефала-5 без расширений, также вытекает из требований минимализма, тем более, что Рефал-5 с расширениями уже есть — Рефал-5λ. Поэтому, когда Рефал-05 создавался на основе Простого Рефала, из последнего была исключена куча возможностей:
WriteLine как
альтернатива Prout, в Рефале-5λ частично сохранено),<s.F e.Arg> (в Рефале-5λ
он есть),e.X^ (в Рефале-5λ оно тоже есть),srmake (в Рефале-5λ она называется rlmake),Тот генератор лексических анализаторов практически не сокращал код лексера
по сравнению с написанием вручную, поэтому из соображений минимализма от него
решено было отказаться. Аналогично, утилита srmake, помогавшая собирать
себя, компилятор и генератор лексеров, тоже оказалась избыточной для такого
маленького проекта.
В Простом Рефале уже был зачаток интерпретации, сделанный Вадимом Сухаревым и Игорем Дрогуновым — его было решено удалить. Интерпретируемый код Дрогунова был добавлен в транслятор когда исходники уже отслеживались в Git, поэтому достаточно было просто аккуратно пересобрать дерево коммитов без фиксаций, посвящённых интерпретации. Это оказалось несложно. Интерпретатор Сухарева удалён из исходников обычным образом.
В отличие от Рефала-5λ и даже от Рефала-5, компилятор Рефала-05 не содержит никаких оптимизаций — это тоже следствие минимализма. Можно добавить оптимизацию общих команд сопоставления — её несложно добавить к сопоставлению с образцом по методу С.А. Романенко, но вряд ли это будет целесообразно. В актуальной реализации сопоставление с образцом не является узким местом (штатный бенчмарк на момент написания этих строк показывает, что сопоставление с образцом занимает 15–18 % времени выполнения, построение результата — 23–29 %, встроенные функции — 17–22 %, копирования t- и e-переменных — 30–34 %). Хотя, исключать добавление тоже нельзя, если код оптимизации окажется простым и компактным.
Стартовой функцией Рефала-5 является функция с именем Go или GO, причём
последнее имя имеет приоритет. Автоматически обеспечить такое поведение
на уровне компоновки Си невозможно (во всяком случае переносимо), поэтому
был сделан ручной выбор входной точки из этих двух.
Библиотека поддержки времени выполнения языка считает, что входная точка —
только GO. Если нужно откомпилировать программу со входной точкой Go, нужно
подключить файл Go.c с такой семантикой:
$EXTERN Go;
$ENTRY GO { = <Go> }
На самом деле, файл Go.c написан вручную на Си и не требует дополнительного
шага рефал-машины (т.е. вызов Go из GO не будет считаться шагом встроенной
функцией Step).
Рефал-05 разрабатывается как простая и эффективная реализация Рефала-5.
В первую очередь, эффективность обеспечивается компиляцией в чистый Си — отсутствует прослойка интерпретации, генерируемый код не ограничен форматом RASL’а.
Генерация кода по алгоритму С.А. Романенко подразумевает наличие двух
переменных — границ текущей дырки отождествления (в диссертации они названы
Г1 и Г2) и инструкции, устанавливающей эти границы (соответственно,
SETГ). Такой подход упрощает язык сборки — не нужно предусматривать
в формате инструкций отождествления элементов границ дырки, в которых они
отождествляются, но при этом ограничивает возможности оптимизации —
целесообразнее доотождествить текущую дырку до упора вместо того, чтобы
многочисленными инструкциями SETГ прыгать туда-сюда. При генерации кода
на Си команда отождествления компилируется в вызов функции рантайма с явной
передачей аргументов, что позволяет команды отождествления разных дырок
располагать в произвольном порядке. Следствие: компилятор может начинать
отождествление с «лёгких» команд (сопоставления с константными символами),
оставляя «тяжёлые» (повторные t- и e-переменые) на потом.
Компилятор использует классическое представление поля зрения плоскими двунаправленными связными списками, что исключает скрытые затраты времени на сборку мусора или отложенное копирование при подсчёте ссылок. Программист при написании кода может точно оценивать производительность, поскольку все временны́е затраты очевидны (циклы по открытым переменным, копирования переменных).
Код компилятора простой и компактный, что обеспечивает быструю компиляцию программ. При использовании быстрого компилятора Си (автор рекомендует BCC версии 5.5.1) компиляция занимает секунды.
Про то, что реализация метафункций сделана с использованием нагруженных идентификаторов в том числе и из соображений эффективности, подробно написано ранее.
Ещё для Простого Рефала была выбрана разработка самоприменимого компилятора, подход был сохранён и для Рефала-05.
Впрочем, разработка самоприменимого компилятора — не панацея, в компиляторе
регулярно находятся ошибки, подтверждением чему является коллекция регрессионных
тестов в папке autotests.
Самая первая версия Простого Рефала (001) была только модельной реализацией —
могла только самоприменяться. Для вызова компилятора C++ использовал команду
call_cpp_compiler, которая вызывала .bat-файл, лежащий в текущей папке.
Стандартные пути поиска не поддерживались — для компилируемых файлов нужно
было указывать полные пути. Из арифметических функций поддерживались только
Add и Sub, поскольку только они использовались в исходниках. «Встроенные»
функции Простого Рефала находились прямо в refalrts.cpp, т.е. библиотека
не была выделена в отдельный файл.
Такой вариант, конечно, прост и минималистичен, но сразу превращает компилятор в совсем бесполезную игрушку. Поэтому решено было сохранить (а позже развить) возможность использовать Рефал-05 как практический инструмент. Но при этом достичь минимализма.
Чтобы упростить чтение командной строки, параметры «используемый компилятор Си»
и «пути поиска» стали читаться из переменных окружения, соответственно,
R05CCOMP и R05PATH. При этом, пути поиска R05PATH автоматически
добавляются к строке запуска сишного компилятора как каталоги include. Для
этого используется опция -Iпуть, которая поддерживается всеми известными
автору компиляторами.
Если Рефал-05 установлен в соответствии с README.md, то для компиляции файла
my_program.ref достаточно ввести в командной строке
refal05c my_program ‹другие исходники› refal05bif refalrts
И будет создан исполнимый файл с именем my_program.exe, a.exe или a.out
в зависимости от используемого компилятора Си.
Это значит, что неподготовленный человек (но знающий Рефал-5), может целиком разобраться во всех исходных файлах компилятора и библиотек за сравнительно небольшое время (меньше недели, условно).
В первую очередь, исходники для этого должны быть компактными, что сочетается
с третьей целью — минималистичностью. На момент написания этих строк (2026-08-09)
исходники компилятора (src/*.ref) и библиотек (refalrts.*, refal05bif.c)
занимают 6534 строки кода (плюс 2943 строки используемых исходников
из refal-5-framework).
Во-вторых, сам код должен быть читабельным. Для этого нужно, чтобы
Поэтому в коде используются только «говорящие» имена переменных (никаких s.K,
e.0, t.1 и т.д.), жёстко заданы правила расстановки скобок и отступов,
префиксы и суффиксы для важных категорий функций и переменных и т.д.
Полностью правила оформления кода описаны в Приложении A (на самом деле они заимствованы из Рефала-5λ).
В исходных текстах компилятора почти все функции — чистые. Побочный эффект
эксплуатируется только на верхнем уровне — лексер (а вместе с ним и парсер)
загружает текст из указанного файла, генератор сохраняет в файле сформированные
строки кода на Си. Модуль верхнего уровня main.ref единственный выводит
на экран сообщения, читает аргументы командной строки, читает переменные
окружения и вызывает компилятор Си при помощи функции System. А всё остальное
чистое.
Код в некоторых местах можно было бы сократить, используя глобальное хранилище, но, по мнению автора, это ухудшило бы читаемость и понимаемость.
В-третьих, работа компилятора Рефала-05 подробно документирована (четвёртая часть руководства). Документация позволяет не только писать собственные внешние функции на Си, но фактически делает Рефал-05 учебным пособием по написанию компиляторов Рефала! Для автора когда-то таким учебным пособием послужила документация к Рефалу-2, здесь же рассмотрен более современный диалект, а также рассмотрено не только устройство рантайма, но и принципы генерации кода.
Рефал-05 педантичен. Это касается не только языка, компилятора и библиотек, но и особенностей разработки.
Рефал-05 у́же, чем Рефал-5 (подробности во второй части):
$EXTERN.Arg в Рефале-05 — <Arg s.NUMBER>, в Рефале-5 — <Arg s.NUMBER e.ANY>,
часть аргумента после номера игнорируется.Особенности компилятора:
Особенности библиотеки:
FILENAME_MAX вместо динамического выделения при помощи malloc().R05_NORETURN и R05_NORETURN_DEFINED.assert’ы, которые по умолчанию включены.r05_switch_default_violation(value),
который рекомендуется помещать в ветку default: оператора ветвления,
если тот никогда не должен выполняться. Он тоже является утверждением.r05_builtin_error и r05_builtin_error_errno принимают
дополнительные параметры в стиле printf, при этом, если компилятор Си
умеет выдавать предупреждения на ошибки в форматной строке для printf,
он будет выдавать и для вызовов этих функций.Особенности разработки:
Компилятор должен работать под современными популярными платформами: Windows NT (т.е. не Windows 9x/ME), Linux и macOS. Поэтому для его раскрутки используются сторонние переносимые компиляторы Рефала: Рефал-5 и Рефал-5λ.
Также ради большей переносимости вместо специализированных утилит сборки (make,
CMake и др.) используются скрипты командного интерпретатора — .cmd-файлы для
Windows и bash-скрипты для POSIX. Утилиты make, идущие в составе с разными
компиляторами (GNU make, nmake для Visual C++…), понимают разный синтаксис
Makefile’ов. А описать скрипт самоприменения makeself.* скорее всего будет
трудно описать столь же лаконично и прозрачно с применением CMake и других
подобных инструментов.
Задача поддержки неактуальных архитектур, DOS и Windows 9x/ME, не ставилась.
Поэтому .cmd-файлы писались в синтаксисе cmd.exe, а не command.com. Хотя
автор не исключает, что Рефал-05 можно собрать и использовать на Windows 9x/ME.
На DOS реального режима компилятор самопримениться не сможет, поскольку требует
9 Мбайт памяти — текст генерируемых файлов на Си сначала целиком формируется
в поле зрения, а потом сохраняется в файл. Переделать можно, но не нужно.