В этом приложении содержатся краткие сведения о расширениях языка Рефала-5λ и инструментах командной строки для разработки.
Медвед! Самый первый заголовок, четвёртого уровня. Оглавлялка должна на нём не упасть.
Последнюю версию дистрибутива всегда можно скачать со страницы репозитория:
https://github.com/bmstu-iu9/refal-5-lambda/releases/latest
Поддерживаются платформы Windows, Linux и macOS, а также имитаторы POSIX-окружения на Windows (Cygwin, MinGW)
Возможна установка в трёх вариантах:
-
Автоматическая установка
-
Установка из полускомпилированных исходников
Полускомпилированные исходники — это оттранслированные исходники Рефала-5λ в C++, которые затем можно развернуть на поддерживаемых платформах (Windows, Linux, macOS), используя компилятор C++
-
Установка из исходников
- Скачайте по ссылке выше инсталлятор, его имя имеет вид
setup-refal-5-lambda-***.exe
. - Запустите его.
- Компилятор по умолчанию установится в
%APPDATA%\Refal-5-lambda
. - Папка
%APPDATA%\Refal-5-lambda\bin
пропишется в%PATH%
. - Папка
%APPDATA%\Refal-5-lambda\lib
пропишется в%RL_MODULE_PATH%
.
Компилятор готов к использованию.
Если на машине установлен компилятор C++, то можно сконфигурировать Рефал-5λ для работы с ним. Тогда программы можно будет компилировать не только в байткод, но и в C++ и собирать их компилятором C++. Но это делать не обязательно. Как это сделать — в следующем параграфе.
- Установите компилятор C++, узнайте, как им пользоваться из командной строки
- Запустите в командной строке
rlc --scratch
. На экране высветится предложение отредактировать файл, по умолчанию%APPDATA%\Refal-5-lambda\c-plus-plus.conf.bat
. - В указанном файле раскомментируйте строчки для для соответствующего
компилятора C++. Поддерживаются MS Visual C++, BCC 5.5.1, MinGW GCC C++,
Watcom, Clang. Можно использовать и другие компиляторы (однако, на них
не тестировалось), для этого нужно правильно настроить соответствующие
переменные среды (
%CPPLINEE%
и%CPPLINEL%
должны заканчиваться на опцию указания имени целевого файла, обычно это опция-o
).
Полускомпилированные исходники — это оттранслированные исходники Рефала-5λ в C++, которые затем можно развернуть на поддерживаемых платформах (Windows, Linux, macOS), используя компилятор C++
- Скачайте архив исходников (
bootstrap-refal-5-lambda-***.zip
илиbootstrap-refal-5-lambda-***.tar.gz
) по ссылке выше, распакуйте в любую папку. - Запустите
bootstrap.bat
. Он создаст файлc-plus-plus.conf.bat
и попросит его отредактировать. Как это сделать, описано в предыдущем параграфе. - Снова запустите
bootstrap.bat
- Добавьте путь к папке
bin
в%PATH%
, к папкеlib
— в%RL_MODULE_PATH%
.
- Склонируйте репозиторий командой
Внимание! Скачивать архив с исходниками нельзя, не соберётся. Сборка рассчитана на использование Git.
git clone https://github.com/bmstu-iu9/refal-5-lambda
- Запустите
bootstrap.bat
. Он создаст файлc-plus-plus.conf.bat
и попросит его отредактировать. Отредактируйте его, как описано на два параграфа выше. - Снова запустите
bootstrap.bat
илиbootstrap.bat --no-tests
. Скриптbootstrap.bat
по умолчанию запускает автоматизированные тесты, выполнение которых может потребовать полчаса-час работы. Ключ--no-tests
предотвращает запуск тестов. - Добавьте путь к папке
bin
в%PATH%
, к папкеlib
— в%RL_MODULE_PATH%
.
На компьютере должен быть установлен компилятор GCC C++ или Clang в режиме
имитации GCC. В любом случае должна работать команда g++
в командной
строке.
Для установки выполните в командной строке команду
curl -L https://bmstu-iu9.github.io/refal-5-lambda/setup.bash | bash
- Компилятор установится в
~/.local/share/refal-5-lambda
- В файл
~/.bashrc
(на Linux) или~/.bash_profile
(на macOS) запишутся команды, добавляющие папки к переменным среды:~/.local/share/refal-5-lambda/bin
в$PATH
,~/.local/share/refal-5-lambda/lib
вRL_MODULE_PATH
.
Компилятор готов к использованию.
Рефал-5λ будет по умолчанию сконфигурирован на работу с компилятором GCC C++.
Если желаете изменить, отредактируйте файл
~/.local/share/refal-5-lambda/c-plus-plus.conf.sh
, указав в нём другой
компилятор или другие опции командной строки (например, уровень оптимизации).
- Скачайте архив исходников (
bootstrap-refal-5-lambda-***.zip
илиbootstrap-refal-5-lambda-***.tar.gz
) по ссылке выше, распакуйте в любую папку. - Установите флаг исполнимости у файла
bootstrap.sh
:chmod +x bootstrap.sh
- Запустите
./bootstrap.sh
. Он скомпилирует исходники, используяg++
в качестве компилятора C++. Если хотите использовать другой компилятор, отредактируйте файлc-plus-plus.conf.sh
и заново запустите сборку. Файлc-plus-plus.conf.sh
по умолчанию отсутствует, появляется только после первого запуска./bootstrap.sh
. - Добавьте путь к папке
bin
в$PATH
, к папкеlib
— в$RL_MODULE_PATH
.
-
Склонируйте репозиторий командой
git clone https://github.com/bmstu-iu9/refal-5-lambda
Внимание! Скачивать архив с исходниками нельзя, не соберётся. Сборка рассчитана на использование Git.
-
Запустите
./bootstrap.sh
или./bootstrap.sh --no-tests
. Он скомпилирует исходники, используяg++
в качестве компилятора C++. Если хотите использовать другой компилятор, отредактируйте файлc-plus-plus.conf.sh
и заново запустите сборку.Файл
c-plus-plus.conf.sh
по умолчанию отсутствует, появляется только после первого запуска./bootstrap.sh
.Скрипт
bootstrap.sh
по умолчанию запускает автоматизированные тесты, выполнение которых может потребовать полчаса-час работы. Ключ--no-tests
предотвращает запуск тестов. -
Добавьте путь к папке
bin
в$PATH
, к папкеlib
— в$RL_MODULE_PATH
.
В поставку компилятора входят две основные утилиты командной строки: rlc
и rlmake
, которыми вы и будете пользоваться. Есть ряд и других утилит
(rl-lexgen
, rl-rsl-decompiler
), мы их рассматривать не будем.
Утилита rlc
выполняет компиляцию, утилита rlmake
— поиск зависимых
файлов и запуск компилятора. Каждая из этих утилит «двухслойная» — состоит
из «ядра» rlc-core
и rlmake-core
, которое вызывается из скрипта
(bat-файлы на Windows, Bash-скрипты на Linux). Сами ядра ничего не знают
о своём окружении (расположении стандартных библиотек, расширениях исполнимых
файлов и динамических библиотек, имени файла стандартного вступления,
имена префиксов и некоторые другие).
Настройкой окружения занимаются скрипты.
Внимание! На Windows утилиты rlc
и rlmake
являются bat-файлами
(соответственно, rlc.bat
и rlmake.bat
), поэтому из файлов сценариев
их нужно вызывать командой call
: call rlc.bat …
.
Скрипты rlc
и rlmake
понимают ограниченное количество опций командной
строки (причём их опции должны быть самыми первыми), остальные аргументы
они как есть передают в rlc-core
и rlmake-core
.
Для того, чтобы собрать программу из нескольких исходных текстов, достаточно написать в командной строке
rlc file1.ref file2.ref file3.ref …
Компилятор создаст исполнимый файл, имя которого совпадает с именем первого
исходника: file1.exe
на Windows или file1
на Linux.
Заметим, что для создания исполнимого файла не требуется компилятор C++,
rlc
по умолчанию компилирует файлы в байткод и формирует исполнимый файл
путём конкатенации готового интерпретатора с байткодом. Интерпретатор
при запуске открывает свой исполнимый файл для чтения, находит в нём
байткод и его выполняет.
Замечание. Аналогичным образом устроены самораспаковывающиеся (SFX) архивы — в них к распаковщику приписывается файл архива.
В одном из файлов исходного кода должна быть определена функция Go
или
GO
, причём GO
имеет приоритет. Её интерпретатор вызывает с пустым
аргументом.
Перечислять исходники в явном виде не всегда удобно, поэтому для облегчения
сборки программ из нескольких исходных файлов предусмотрена утилита rlmake
.
Она получает из командной строки единственный файл и сканирует его в поисках
меток *$FROM ИмяФайла
. Эта метка означает, что также нужно подключить
к сборке файл с указанным именем и его тоже рекурсивно просканировать.
Получив набор всех зависимых файлов, утилита rlmake
вызывает rlc-core
для компиляции программы.
Метки *$FROM
рекомендуется размещать перед директивами $EXTERN
, в которых
перечисляются импортируемые имена функций из соответствующего исходного
файла. Пример:
*$FROM DisplayName
$EXTERN DisplayCName, CName;
*$FROM Escape
$EXTERN EscapeChar, EscapeString;
Скрипты rlc
и rlmake
распознают только несколько ключей командной строки,
причём эти ключи должны располагаться в начале. Остальные аргументы как есть
передаются rlc-core
и rlmake-core
.
Ключи, понимаемые скриптами:
- Выбор интерпретатора-префикса:
--slim
(по умолчанию) — «тонкий» префикс, включающий в себя только библиотекиLibrary
иPlatform
.--rich
— «толстый префикс», содержащийLibrary
,Platform
,LibraryEx
,GetOpt
,Hash
.--scratch
— префикса нет, режим компиляции в C++ с последующим вызовом компилятора C++.
- Компоновка библиотек:
--auto
(по умолчанию) — статическая при сборке исполнимых файлов, динамическая при сборке библиотек (.dll
,.so
,.rasl-module
).--static
— всегда статическая.--dynamic
— всегда динамическая.
- Средства отладк:
--debug
— использование отладочного рантайма.--no-debug
(по умолчанию) — рантайм без отладочных средств.
Все три сорта опций можно комбинировать произвольным образом + все они
допустимы и при компиляции в исполнимые файлы, и при компиляции в динамически
загружаемые библиотеки (.dll
, .so
, .rasl-module
).
В режиме --scratch
целевая программа должна собираться как из пользовательских
исходников, так и из исходных файлов рантайма. При использовании rlc
все
необходимые файлы рантайма необходимо указывать в командной строке, а их состав
не документирован и может меняться в разных версиях. При использовании rlmake
все требуемые файлы рантайма будут найдены автоматически по зависимостям.
Поэтому не рекомендуется использовать --scratch
вместе с rlc
.
Скрипты rlc
и rlmake
поддерживают переменные среды RLC_FLAGS
и RLMAKE_FLAGS
соответственно. Значения этих переменных автоматически
добавляются к командным строкам запуска rlc-core
и rlmake-core
. Их можно
использовать или если одни и те же параметры приходится задавать часто,
или если rlc
или rlmake
вызываются из какого-то другого скрипта и задать
там дополнительные опции невозможно.
Дальнейшее описание в равной степени относится и к rlc
, и к rlmake
. Там,
где упоминается опция компилятора -Od
, подразумевается что эта опция
«пробрасывается» от rlmake
до rlc-core
при помощи ключа -X
: -X-Od
.
Компилятор может создавать исполнимые файлы, динамически загружаемые библиотеки или вообще производить компиляцию без компоновки.
По умолчанию компилятор создаёт исполнимый файл — файл, который можно запустить
непосредственно. На Windows он будет иметь расширение .exe
, на unix-like —
без расширения, но с флагом исполнимости. Этот режим используется по умолчанию,
хотя для него можно явно задать опцию -x
или --makeexe
.
В режимах --slim
и --rich
компилятор по умолчанию конфигурируется для
создания целевого файла из байткода, который приписывается к готовому
интерпретатору-префиксу.
В режиме--scratch
компилятор вызывает C++ для сборки префикса из файлов C++
(как библиотечных, в том числе рантайма), так и построенных из пользовательских
исходных текстов (если в них использовались нативные вставки или использовался
режим -Od
).
Для сборки библиотеки используется опция -l
или --makelib
. В этом случае,
если в исходных файлах не было нативных вставок и не использовался режим
-Od
, строится файл, целиком состоящий из байткода и имеющий расширение
.rasl-module
. В случае использования нативных вставок или режима -Od
вызывается компилятор C++ (т.е. необходимо использовать опцию --scratch
)
и строится целевой файл с расширениями .dll
(на Windows) или .so
(на unix-like).
Опция -R
предписывает создавать только .rasl-module
и не совместима
с ключами -Od
или нативными вставками в исходных файлах.
Опция -C
включает режим «только компиляция». Целевой файл не создаётся,
вместо этого для каждого файла исходного текста создаётся объектный
файл с байткодом с расширением .rasl
. Для файлов с нативными вставками
также создаётся файл .cpp
с текстом из нативных вставок. В режиме -Od
для каждого исходного файла создаётся пара файлов .rasl
+ .cpp
.
Вообще, файлы .rasl
по умолчанию создаются всегда, только в режимах
--makeexe
, --makelib
и -R
они подтираются при завершении компиляции.
Среди исходных файлов могут быть не только исходные тексты, но и уже
скомпилированные файлы .rasl
или пары .rasl
+ .cpp
. В этом случае
они «прилинковываются» к готовой программе.
Утилита srmake
для файлов .rasl
без исходников (.ref
, .sref
)
ищет одномённый файл с расширением .froms
и ищет там метки зависимостей
(в файлах .froms
они должны иметь вид //FROM ИмяФайла
). Кроме того,
на метки зависимостей сканируются .cpp-файлы (тоже //FROM
).
Компилятор rlc
рассматривает в качестве исходных файлов файлы байткода
для классического Рефала-5 (файлы с расширением .rsl
). Если среди
исходных файлов таковой обнаружится, то компилятор сначала вызовет утилиту
rl-rsl-decompiler
, а затем будет компилировать полученный файл.
Утилита rlmake
для .rsl-файлов также ищет файлы .froms
.
Опции -d
и -D
определяют папки поиска исходных файлов. Компилятор и утилита
rlmake
, когда встречают имя файла, сначала проверяют его наличие в текущей
папке (так, как записано, с расширениями .ref
, .sref
, .rsl
и .rasl
),
затем по очереди в каждой из папок поиска. Опция -D
, в отличие от -d
,
сообщает компилятору, что данная папка также является хранилищем заголовочных
файлов рантайма и должна быть передана компилятору C++ опцией -I
(путь поиска
#include
-файлов).
Опции -d
и -D
имеют длинные синонимы --dir
/--directory
и --runtime-dir
/--runtime-directory
соответственно.
В этих же путях поиска ищутся заголовочные файлы для директивы $INCLUDE
.
Ключ --ref5rsl
предписывает добавить к путям поиска пути из переменной
окружения REF5RSL
. Эту переменную использует интерпретатор классического
Рефала-5 для поиска .rsl-файлов.
Опция -r имя
(--reference=имя
) сообщает, что создаваемый файл имеет
зависимость от динамически загружаемой библиотеки с данным именем.
Опция -o файл
(--target-file=файл
) задаёт имя целевого файла. Целевой файл
создаётся в точности с тем именем, которое указано — расширение .exe
, .dll
,
.so
не добавляется. Если эта опция не указана, то создаётся файл с именем,
совпадающим с именем первого входного файла в командной строке (единственного
для rlmake
) и расширением по умолчанию.
Есть и другие, более специфичные опции, мы их рассматривать не будем.
В этом разделе мы рассмотрим опции, специфичные для компилятора. Утилита
rlmake
их не поддерживает, но позволяет «прокинуть», используя -Xопция
(или --thru=опция
, или --through=опция
).
-
--grammar-check
— проверяет исходные файлы на синтаксические ошибки, компиляцию не осуществляет. -
--markup-context
— размечает контекст замыканий (см. главу про рассахариватель). -
-Oключи
— ключи оптимизации. Может иметь вид-O...+
— добавить указанные ключи к установленным ранее (знак+
подразумевается по умолчанию, его можно не писать),-O...-
— удалить эти ключи из установленных ранее,-O...=
— забыть о ключах, установленных ранее, установить указанные.
Поддерживаются следующие ключи:
-OP
— оптимизация совместного сопоставления с образцом,-OR
— оптимизация построения результатных выражений,-Od
— оптимизация прямой кодогенерации, вместо байткода генерируется код на C++,-OT
— древесная оптимизация, раскрывает замыкания в точках вызова, неявно подразумевается с флагамиi
,A
,D
,I
,S
.-OD
— оптимизация прогонки, функции, помеченные$DRIVE
, прогоняются, функции, помеченные$INLINE
, встраиваются,-OI
— оптимизация встраивания, встраиваются функции, помеченные и$DRIVE
, и$INLINE
.-OS
— оптимизация специализации,-OA
— автоматическая установка меток$DRIVE
,$INLINE
,$SPEC
,-Oi
— оптимизация встроенных (intrinsic) функций.
-
--prelude=имя
— задаёт имя стандартного вступления (см.$INCLUDE
в главе про синтаксис). По умолчанию —refal5-builtins.refi
. -
--log=имя-файла
— имя файла с логом трансляции. В лог выписывается синтаксическое дерево после обессахаривателя и на каждом проходе древесной оптимизации (T
,D
,I
,S
,A
,i
), а также императивный псевдокод. -
--opt-tree-cycles=N
— максимальное количество проходов древесной оптимизации. -
--classic
— по умолчанию компилировать в классическом режиме. -
--extended
— по умолчанию компилировать в расширенном режиме.
Программа rlgo
загружает библиотеку (.rasl-module
, .dll
или .so
)
и запускает функцию Go
или GO
в ней (GO
имеет приоритет).
Командная строка:
rlgo [отладочные ключи...] библиотека [аргументы...]
Список отладочных ключей тот же самый, какой допустим для файлов
@refal-5-lambda-diagnostics.ini
, список ключей выводится по команде
--help
.
Рефал-5λ является расширением Рефала-5, т.е. Рефал-5 является его точным подмножеством. Поэтому рекомендуется изучить Рефал-5 по учебнику Турчина:
http://refal.botik.ru/book/html/
и при чтении этого справочника сосредоточиться на отличиях.
В грамматике ниже фигурные скобки означают повторение ноль или более раз,
квадратные скобки — необязательный элемент. Строки в двойных кавычках
($ENTRY
, "="
…) и имена сплошными заглавными (IDENT
) соответствуют
терминальным символам, имена в CamelCase (LeftPart
) — нетерминальным.
Program ::= Unit
Unit ::=
Declaration
| Definition
| "$INCLUDE" COMPOUND ";"
| NATIVE-CODE
| ";"
Declaration ::=
"$EXTERN" NameList
| "$ENTRY" NameList
| "$DRIVE" NameList
| "$INLINE" NameList
| "$SPEC" Name Pattern ";"
| "$LABEL" NameList
Name ::= IDENT
NameList ::= Name { "," Name } ";"
Definition ::=
["$ENTRY"] Name Body
| "$ENUM" NameList
| "$EENUM" NameList
| "$META" NameList
| "$SWAP" NameList
| "$ESWAP" NameList
Body ::=
"{" [Sentences] "}"
| "{" NATIVE-BODY "}"
Sentences ::= Sentence { ";" Sentence } [";"]
Sentence ::= LeftPart RightPart
LeftPart ::= Pattern { Condition }
Condition ::= "," ResultEx ":" Pattern
RightPart ::=
{ Assignment } "=" ResultEx
| { Assignment } "," ClassicBlock
Assignment ::= "=" ResultEx ":" LeftPart
ResultEx ::= Result { ":" Body }
ClassicBlock ::= Result ":" Body
Pattern ::= { PatternTerm }
PatternTerm ::=
Symbol
| VARIABLE ["^"]
| "(" Pattern ")"
| "[" Name Pattern "]"
Symbol ::= IDENT | COMPOUND | "&" Name | CHAR | NUMBER
Result ::= { ResultTerm }
ResultTerm ::=
Symbol
| VARIABLE
| "(" Result ")"
| OpenCall Result ">"
| "[" Name Result "]"
| "{" Body "}"
OpenCall ::= "<" [Name] | "<+" | "<-" | "<*" | "</" | "<%" | "<?"
Пробелы, табуляции и переводы строк считаются символами пустого пространства. Комментарии бывают однострочными и многострочными.
Однострочный комментарий представляет собой строку, в первой колонке
которой находится знак *
.
* Это комментарий
Многострочный комментарий такой же, как и в Си — /* … */
, допусти́м
в любом месте, где допусти́м пробельный символ.
Псевдокомментарий *$CLASSIC
переключает синтаксический анализ в режим
совместимости с классическим Рефалом-5, расширения Рефала-5λ будут трактоваться
как синтаксические ошибки. Псевдокомментарий *$EXTENDED
обратен предыдущему,
разрешает в последующем тексте расширенные возможности Рефала-5λ. Стартовый
режим устанавливается опциями --classic
и --extended
(по умолчанию).
Остальные лексемы будут рассматриваться в процессе обзора грамматики.
Рассмотрим подробнее основные элементы грамматики.
Program ::= Unit
Unit ::=
Declaration
| Definition
| "$INCLUDE" COMPOUND ";"
| NATIVE-CODE
| ";"
Программа — это последовательность объявлений, определений, директив включения заголовка, вставок нативного кода и точек с запятой.
Далее по тексту ключевые слова со знаком доллара, начинающие конструкции языка и сами эти конструкции мы будем называть директивами.
Директива $INCLUDE
предписывает включить в исходный файл текст из данного
заголовочного файла. Заголовочные файлы имеют расширение .refi
(синтаксис
Рефала-5λ) или .srefi
(синтаксис Простого Рефала, deprecated). При этом
расширение можно не писать, расширение будет добавлено автоматически (.refi
имеет приоритет). Поиск осуществляется в каталогах поиска, задаваемых опциями
-d
или -D
командной строки. Рекурсивная зависимость включаемых файлов
не является ошибкой.
Исходный файл после вставки всех заголовочных файлов становится единицей
трансляции. Единица трансляции компилируется в файл интерпретируемого кода
с расширением .rasl
. Интерпретируемый код для Рефала традиционно называется
RASL — Refal ASsembly Language.
В каждый исходный файл Рефала-5λ компилятор неявно добавляет директиву
$INCLUDE
, подключающую стандартное вступление, содержащее объявления
и определения встроенных функций Рефала-5.
Вставка нативного кода — фрагмент текста, между строчками %%
и %%
. Пример:
%%
#include <stdio.h>
%%
Если файл содержит вставки нативного кода (в данной реализации — C++), то вместе с файлом RASL’а компилятор создаст исходник на C++, куда поместит текст всех нативных вставок. О компиляции в нативный код будет написано подробнее в соответствующей главе.
Точки с запятой игнорируются. Программа слева эквивалентна программе справа:
;;;;;;; |
|
$EXTERN Map;;;;;; | $EXTERN Map;
|
$ENTRY Go { | $ENTRY Go {
= <Map &Prout Hello World> | = <Map &Prout Hello World>
};;;;;;;; | }
|
;;;;;;; |
- Классический Рефал-5 не поддерживает включение заголовочных файлов и вставки нативного кода.
- Встроенные функции в нём буквально встроены. В Рефале-5λ при желании можно расширить стандартное вступление своими функциями.
Declaration ::=
"$EXTERN" NameList
| "$ENTRY" NameList
| "$DRIVE" NameList
| "$INLINE" NameList
| "$SPEC" Name Pattern ";"
| "$LABEL" NameList
Name ::= IDENT
NameList ::= Name { "," Name } ";"
Объявление не создаёт нового объекта, а только задаёт свойства имеющимся, либо пополняет область видимости.
Имя функции является идентификатором. Идентификатор в Рефале-5λ начинается на заглавную или строчную латинскую букву или прочерк, состоит из латинских букв, цифр, прочерков и дефисов:
Go, Hello, __Step-Drop, cRaZy-_-nAmE
Директива $EXTERN
добавляет в область видимости текущего файла перечисленные
имена. Как правило, это имена entry-функций (см. далее), описанные в других
единицах трансляции. Но объявление $EXTERN
для имени в текущей области
видимости ошибкой не является. Вместо $EXTERN
можно писать $EXTERNAL
или
$EXTRN
.
Директива $ENTRY
говорит о том, что функции с данными именами являются
entry-функциями. Т.е. код слева эквивалентен коду справа
Foo { … } | $ENTRY Foo { … }
|
$ENTRY Foo, Bar; |
|
Bar { … } | $ENTRY Bar { … }
Директивы $INLINE
и $DRIVE
описывают функции, которые компилятор может
встроить или прогнать соответственно. Директива $SPEC
описывает шаблон
специализации для функции. Мы не будем их рассматривать в этом документе.
Функции, имена которых указываются в $ENTRY
, $DRIVE
, $INLINE
, $SPEC
,
должны быть определены в той же единице трансляции.
Директива $LABEL
описывает имена идентификаторов, которые можно использовать
в нативных вставках.
- Классический Рефал-5 поддерживает только объявление
$EXTERN
и его синонимы$EXTRN
и$EXTERNAL
. - Идентификаторы в классическом Рефале-5 не могут начинаться на прочерк.
Definition ::=
["$ENTRY"] Name Body
| "$ENUM" NameList
| "$EENUM" NameList
| "$META" NameList
| "$SWAP" NameList
| "$ESWAP" NameList
Определение определяет функцию.
Конструкция ["$ENTRY"] Name Body
является основным способом определения
функции. Если указано ключевое слово $ENTRY
, определяемая функция становится
entry-функцией — она становится доступной для вызова из других единиц
трансляции. Если ключевое слово $ENTRY
перед определением остутствует и имя
функции не указано в списках $ENTRY
, то функция является локальной.
Директивы $ENUM
и $EENUM
определяют пустые функции — функции без
предложений. Директива $ENUM
определяет локальные пустые функции, $EENUM
(entry enum) — пустые entry-функции.
Программа
$ENUM Foo, Bar;
$EENUM Baz;
эквивалентна следующей:
Foo {}
Bar {}
$ENTRY Baz {}
Пустые функции в текущей реализации используются только для объявления меток квадратных скобок (см. далее).
Директива $META
определяет метафункцию — функцию, которая работает с текущей
областью видимости. Например, метафункция Mu
вызывает функцию из текущей
области видимости по указанному имени. Метафункция определяется как локальная.
В теле метафункции вызывается внешняя функция с именем вида __Meta_NAME
(где NAME
— имя метафункции), которой передаётся исходный аргумент
и описатель области видимости.
Директивы $SWAP
и $ESWAP
описывают статические ящики — функции, способные
хранить некоторое значение между вызовами. Эти функции могут принимать любой
аргумент и при вызове возвращают предыдущий аргумент вызова. Самый первый вызов
всегда возвращает пустоту. По аналогии с $ENUM
и $EENUM
, директивы $SWAP
и $ESWAP
определяют локальные и entry статические ящики.
Пример.
$SWAP S;
$ENTRY Go {
/* пусто */
= <Prout '1: [' <S 'one'> ']'>
<Prout '2: [' <S 'two'> ']'>
<Prout '3: [' <S 'three'> ']'>
}
Эта программа напечатает
1: []
2: [one]
3: [two]
- Директивы
$ENUM
,$EENUM
,$SWAP
,$ESWAP
и$META
являются расширениями Рефала-5λ.
Body ::=
"{" [Sentences] "}"
| "{" NATIVE-BODY "}"
Sentences ::= Sentence { ";" Sentence } [";"]
Тело функции записывается в фигурных скобках. Внутри них может быть записана либо последовательность предложений (возможно, пустая), либо нативная вставка.
Если функция описана как последовательность предложений, то выбирается первое предложение, сопоставимое с аргументом, и это предложение выполняется. Если такового предложения не нашлось, программа аварийно останавливается.
Предложения разделяются точкой с запятой. После последнего предложения допустимо ставить необязательную точку с запятой.
Если функция описана с пустым телом (явно с пустыми фигурными скобками
или при помощи директив $ENUM
и $EENUM
), то в ней по определению
нет подходящего предложения, а значит, её вызове программа аварийно
останавливается всегда.
Если тело функции описано вставкой нативного кода, то компилятор создаст функцию на целевом языке (в данной реализации — C++), куда поместит содержимое нативной вставки. Эта функция будет выполняться при активизации тела функции на Рефале.
- Пустые функции в классическом Рефале-5 синтаксически недопустимы, должно быть хотя бы одно предложение.
- Классический Рефал-5 не поддерживает нативные вставки.
Sentence ::= LeftPart RightPart
LeftPart ::= Pattern { Condition }
Condition ::= "," ResultEx ":" Pattern
RightPart ::=
{ Assignment } "=" ResultEx
| { Assignment } "," ClassicBlock
Assignment ::= "=" ResultEx ":" LeftPart
ResultEx ::= Result { ":" Body }
ClassicBlock ::= Result ":" Body
Предложение — правило, описывающее вычисление функции при заданных ограничениях на аргумент.
Предложение состоит из левой части и правой части. Правая часть, как правило,
начинается со знака =
, но иногда может начинаться и на ,
(запятую).
Левая часть описывает ограничения, налагаемые на некоторое выражение, и, возможно, окружение. Она состоит из образца, за которым может следовать ноль или более условий.
Образец описывает шаблон — выражение с перееменными. Сопоставление некоторого выражения с образцом является успешным, если удалось найти подстановку переменных образца, переводящую его в аргумент в данное выражение. Образец левой части сопоставляется с аргументом функции.
Условие описывает дополнительные ограничения. Оно начинается на знак ,
и состоит из пары — расширенного результатного выражения (левой части условия)
и образца (правой части условия), разделённых знаками :
.
Во время проверки условия вычисляется значение её левой части и затем оно сопоставляется с образцом. Условие выполняется, если удалось сопоставить значение левой части с образцом.
Выражение успешно сопоставилось с левой частью, если оно сопоставилось с образцом и выполнились все условия. Условия проверяются последовательно слева направо. Если сопоставления с образцами (первым и/или образцами условий) неоднозначны, то допустимые подстановки перебираются в определённом порядке, о котором будет сказано далее: для каждого из образцов выбирается та, при которой выполнились все последующие условия.
Левая часть предложения сопоставляется с образцом. Если левая часть сопоставилась успешно, управление передаётся на правую часть предложения. Если нет — проверяется следующее предложение. Если предложение было последним и левую часть не удалось сопоставить с образцом, программа аварийно завершается.
Пример. Функция Abracadabra
вернёт Abra
, если аргумент начинается на 'A'
и файл config.ini
существует. Иначе вернёт Cadabra
.
Abracadabra {
'A' e._, <ExistFile 'config.ini'> : True = Abra;
e._ = Cadabara;
}
Левая часть первого предложения 'A' e._, <ExistFile 'config.ini'> : True
говорит о том, что аргумент должен начинаться на 'A'
(образец 'A' e._
)
и выражение <ExistFile 'config.ini'>
сопоставимо с образцом True
.
Левая часть второго предложения описывается образцом e._
без условий.
Образец e._
успешно сопоставляется с любым предложением.
Правая часть состоит из нуля или более присваиваний, за которыми следует
либо знак =
и расширенное результатное выражение, или знак ,
, за которым
следует классический блок.
При выполнении правой части сначала последовательно выполняются присваивания, после чего вычисляется значение расширенного результатного выражения или классического блока.
Присваивание начинается на знак =
, за которым следуют расширенное
результатное выражение и левая часть, разделённые знаком :
. Расширенное
результатное выражение вычисляется и его значение сопоставляется с левой
частью. Сопоставление должно быть успешным. При неуспешном сопоставлении
с левой частью программа аварийно завершается.
Присваивания используются для того, чтобы вычислить некоторое значение и связать его с переменными, также иногда используются ради побочного эффекта.
Расширенное результатное выражение состоит из результатного выражения,
за которым следует ноль или более тел функций, предварённых знаками :
.
Значение результатного выражения последовательно преобразуется путём
вызова вложенных функций, описаных их телами.
Расширенное результатное выражение вида
…res… : { …f1… } : { …f2… } … : { …fn−1… } : { …fn… }
эквивалентно следующей цепочке присваиваний, оканчивающихся результатным выражением:
…res… : e.1
= <{ …f1… } e.1> : e.2
= <{ …f2… } e.2> : e.3
…
= <{ …fn−1… } e.n−1> : e.n
= <{ …fn… } e.n>
или следущему результатному выражению:
<{ …fn… } <{ …fn−1… } … <{ …f2… } <{ …f1… } …res…>>…>>
Классический блок состоит из результатного выражения и тела функции. Его семантика эквивалентна семантике расширенного результатного выражения с одним телом функции.
Тела функций в расширенных результатных выражениях и блоках часто называются блоками. Также их можно называть вложенными функциями, хотя этот термин означает тела функций, находящиеся внутри результатных выражений.
Образцовые и результатные выражения могут содержать переменные. При этом
пополнять область видимости переменных могут только образцы (в левых частях,
условиях, присваиваниях). Область видимости переменной начинается с точки
её первого появления в предложении и заканчивается в конце предложения.
Однако, переменная может быть сокрыта новой переменной с тем же именем,
помеченной знаком ^
(см. далее). Внутри блоков и вложенных функций
в результатных выражениях видны все те переменные, которые видны в месте
расположения этого блока (но тоже могут быть сокрыты переменными с ^
).
- В Рефале-5 нет присваиваний и расширенных результатных выражений.
Если в грамматике для предложений заменить
ResultEx
наResult
и убрать присваивания, получим грамматику для предложений классического Рефала-5:Sentence ::= LeftPart RightPart LeftPart ::= Pattern { Condition } Condition ::= "," Result ":" Pattern RightPart ::= "=" Result | "," ClassicBlock ClassicBlock ::= Result ":" Body
Прежде чем описывать синтаксис образцовых и результатных выражений, нужно определить понятие объектного выражения.
Объектное выражение можно описать так:
ObjectExpr ::= { ObjectTerm }
ObjectTerm ::=
RuntimeSymbol
| "(" ObjectExpr ")"
| "[" Name ObjectExpr "]"
RuntimeSymbol ::= CHAR | WORD | NUMBER | FUN-PTR | CLOSURE
Объектные выражения — это данные языка Рефал. Объектные выражения состоят из нуля или более объектных термов.
Объектный терм может быть либо символом — неделимым элементом данных (литерой, словом, числом…), либо составным термом — выражением в круглых или квадратных скобках.
Символ — неделимый (в смысле сопоставления с образцом) элемент данных. В случае Рефала-5λ во время выполнения программы символ может быть литерой, словом, числом, указателем на функцию или замыканием. Смысл различных типов символов будет объяснён позже.
Скобочные термы образуются скобками двух видов — круглые структурные скобки и квадратные АТД-скобки (АТД — абстрактный тип данных).
После открывающей квадратной скобки обязательно должно находиться имя функции из текущей области определения — указатель на эту функцию становится частью скобочного терма. Если упоминается локальная функция, то такой терм можно построить (в результатном выражении) или сопоставить с образцом (в образцовом выражении) только в единице трансляции, где этот терм был объявлен. В других единицах трансляции такой терм может быть сопоставлен только с t-переменной. Так при помощи квадратных скобок осуществляется инкапсуляция данных.
Для создания «запечатанного» терма можно использовать имя любой локальной
функции. Обычно для этой цели специально определяют пустую локальную функцию
при помощи директивы $ENUM
.
Pattern ::= { PatternTerm }
PatternTerm ::=
Symbol
| VARIABLE ["^"]
| "(" Pattern ")"
| "[" Name Pattern "]"
Symbol ::= COMPOUND | IDENT | "&" Name | CHAR | NUMBER
Образец или образцовое выражение описывает некоторое множество объектных выражений. Образец записывается как последовательность символов, переменных, круглых и квадратных скобок, причём скобки должны быть образовывать правильную скобочную структуру.
Символ (symbol) в образцовом выражении может быть записан как идентификатор,
составной символ, имя функции, предварённое знаком &
, литера или число.
Идентификатор и составной символ являются способами записи символов-слов — некоторых строчек знаков, записываемых как единое целое. Составной символ записывается как строка в двойных кавычках, которая может содержать escape-последовательности:
\'
— одинарная кавычка,'
,\"
— двойная кавычка,"
,\\
— обратная косая черта,\
,\n
— перевод строки, LF,\r
— возврат каретки, CR,\t
— знак табуляции,\xHH
— символ с кодомHH
в шестнадцатеричной записи, например,\x07
— звуковой сигнал,\x0A
— перевод строки (то же, что и\n
), поскольку имеет код 10,\x3F
— знак вопроса?
,\<
,\>
,\(
,\)
— то же, что и<
,>
,(
,)
— добавлены для совместимости с классическим Рефалом-5.
Если символ-слово удовлетворяет синтаксическим требованиям для идентификаторов (состоит только из латинских букв, цифр, прочерков и дефисов и при этом не начинается на дефис), то он может быть записан и идентификатором, и составным символом. Иначе — только составным символом.
Примеры: Hello
— слово Hello
, "Hello"
— тоже самое,
"\x48\x65\x6C\x6c\x6F"
— тоже слово Hello
, "Hello, World!"
— может быть
записано только в кавычках, ""
— пустое слово.
Указатель на функцию записывается как имя функции, предварённое знаком &
.
Функция должна находиться в текущей области видимости. Между &
и именем
допустим знак пробела.
Литера (character) — печатный знак и некоторого символьного набора (в текущей
реализации — ASCII). Записывается в одинарных кавычках. В литерах допустимы те же
escape-последовательности, что и в составных символах. Несколько литер подряд
могут быть записаны слитно, под одними кавычками, например, 'ABC'
означает три
символа, то же, что и 'A' 'B' 'C'
.
Классический Рефал-5 поддерживает недокументированную возможность —
escape-последовательность без кавычек. Т.е. запись \n
в тексте программы
означает то же, что и '\n'
. Рефал-5λ поддерживает и эту возможность.
Число или макроцифра — целое число в диапазоне от 0 до 4 294 967 296 (2³²−1).
Переменная записывается как 〈тип〉.〈индекс〉
, где 〈тип〉
— один из знаков
s
, t
или e
, 〈индекс〉
— последовательность латинских букв, цифр, знаков
прочерка и дефиса, не может начинаться на дефис. В зависимости от типа
различают s-переменные, t-переменные и e-переменные. Переменная описывает
некоторый участок объектного выражения. S-переменные соответствуют символам,
t-переменные — термам и e-переменые — произвольным выражениям (в том числе,
пустым).
Иногда бывает удобно использовать в присваивании, условии или блоке переменную
с некоторым индексом, но в окружающей области видимости уже есть связанная
переменная с тем же индексом, и её значение больше не нужно. В этом случае
индекс можно использовать повторно, если после переменной записать знак ^
.
Переменная со знаком ^
уже не считается повторной, её имя перекрывает
в области видимости имя внешней переменной до конца предложения. Такие
переменные мы будем называть переопределёнными.
Пример.
Eq {
(e.X) (e.Y)
= e.Y
: {
e.X = True;
e.X^ = False;
}
}
В первом предложении вложенной функции переменная e.X
повторная, во втором —
переопределённая.
Если в образце переопределённая переменная встречается несколько раз, то знак
^
должен быть только у первого вхождения.
Если индекс переменной начинается со знака _
, то такая переменная является
безымянной. Для каждого вхождения безымянной переменной компилятор
создаёт уникальное имя. Поэтому даже если безымянные переменные имеют
одинаковые индексы, они всё равно не равны.
Пример. Условие , A B : s._ s._
и цепочка условий , A : s_, B : s_
всегда
выполняются.
Знак ^
после безымянных переменных запрещён, т.к. бессмысленный.
Если подставить в образец вместо переменных значения соответствующего типа, (причём все вхождения одной и той же переменной заменяются на одно и то же значение), то мы получим некоторое объектное выражение. Таким образом, образец описывает множество объектных выражений, которые можно получить путём подстановок значений вместо переменных.
Пусть даны объектное выражение E
и образцовое выражение P
. Поиск такой
подстановки, которая переводит P
в E
называется сопоставлением
выражения E
с образцом P
и обозначается E : P
.
Сопоставление с образцом может быть неоднозначным. Например, сопоставление
(A B C) (C A A B) : (e.1 s.2 e.3) (e.4 s.2 e.5)
даст следующие подстановки (зна́ком ε
обозначено пустое выражение):
A ← e.1, B ← s.2, C ← e.3, C A A ← e.4, ε ← e.5 (1)
ε ← e.1, A ← s.2, B C ← e.3, C A ← e.4, B ← e.5 (2)
ε ← e.1, A ← s.2, B C ← e.3, C ← e.4, A B ← e.5 (3)
A B ← e.1, C ← s.2, ε ← e.3, ε ← e.4, A A B ← e.5 (4)
Для подстановок определено отношение порядка. Чтобы сравнить две подстановки, нужно выписать длины e-переменных (в термах) в том порядке, в каком переменные упоминаются в образце и сравнить полученные строки чисел в лексиконографическом порядке. В приведённом примере подстановки упорядочатся следующим образом:
(3) < (2) < (1) < (4)
У подстановок (3) и (2) равная длина переменной e.1
, но переменная e.4
у (3) короче.
Result ::= { ResultTerm }
ResultTerm ::=
Symbol
| VARIABLE
| "(" Result ")"
| OpenCall Result ">"
| "[" Name Result "]"
| "{" Body "}"
OpenCall ::= "<" [Name] | "<+" | "<-" | "<*" | "</" | "<%" | "<?"
Symbol ::= IDENT | "&" Name | COMPOUND | CHAR | NUMBER
Результатное выражение описывает способ вычисления новых объектных выражений. Оно состоит из результатных термов, которые могут быть символами, переменными, скобочными термами обоих видов, вызовами функций и вложенными функциями — телами функций в фигурных скобках.
В результатных выражениях безымянные переменные запрещены.
Вызовы функций записываются в угловых скобках. Если справа от открывающей угловой скобки находится имя, то оно должно быть именем функции из текущей области видимости. Если имя отсутствует, то вызываемая функция определяется во время выполнения — первым термом выражения внутри скобок должен быть либо указатель на функцию, либо объект замыкания.
После открывающей угловой скобки может находиться один из следующих знаков:
+
, -
, *
, /
, %
, ?
. Это синтаксический сахар для вызова функций,
соответственно, Add
, Sub
, Mul
, Div
, Mod
, Residue
.
Вложенная функция, записываемая как тело функции в фигурных скобках, при выполнении превращается в объект-замыкание. Этот объект в образце может быть сопоставлен только с s-переменной. Замыкание можно вызвать, поместив его после безымянной открывающей угловой скобки.
Как уже было сказано выше, повторные переменные в образце должны сопоставляться с равными участками объектного выражения. Поэтому необходимо определить понятие равенства.
Инвариант равенства копий. Если два выражения получены путём копирования (подстановки одного и того же значения в кратные переменные в результатном выражении), то они равны (могут быть сопоставлены с кратными переменными в образцовом выражении).
Пример:
F { e.X = <Eq (e.X) (e.X)> }
Eq {
(e.Eq) (e.Eq) = True;
(e.L) (e.R) = False;
}
В соответствии с инвариантом функция F
всегда возвращает True
.
Чтобы обеспечить инвариант с одной стороны, и производительность с другой, вводятся следующие требования к сравнению.
- Символы-литеры, символы-слова, символы-числа сравниваются очевидным образом по значению. Проверка на равенство выполняется за константное время.
- Структурные скобочные термы равны, если равно их содержимое.
- Абстрактные скобочные термы равны, если равны их метки-указатели на функции и равно их содержимое. Т.е. мы имеем ограниченное нарушение инкапсуляции — вне единицы трансляции разобрать закрытые термы не можем, но можем сказать, равны два терма или нет.
- Два указателя на функцию равны, если указывают на одну и ту же функцию. Две функции с одинаковыми именами в разных единицах трансляции (одна из них неизбежно должна быть локальной) являются различными.
- А для замыканий уже интереснее. Для них чёткого критерия нет, определены
лишь «граничные условия»:
- В соответствии с инвариантом все копии одного и того же замыкания всегда равны.
- Два замыкания, построенные из вложенных функций, расположенных в исходном тексте по разным координатам, не равны.
- Два замыкания, построенные из вложенных функций в разных единицах трансляции, не равны. Пояснение. В разные единицы трансляции может включаться один и тот же файл, который порождает замыкание. Эти порождённые замыкания не равны.
- Два замыкания, захватывающие неравный контекст, не равны.
- Остальное не определено.
Пример.
F { e.X = { = e.X } }
G { e.X = { = e.X } }
H { = { e.X = e.X } }
$ENTRY Go {
= <Eq &F &F> /* 1 */
<Eq &F &G> /* 2 */
<Eq <F 'ab'> <G 'ab'>> /* 3 */
<Eq <F 'ab'> <F 'cd'>> /* 4 */
<Eq <F 'ab'> <F 'ab'>> /* 5 */
<Eq <H> <H>> /* 6 */
}
Eq {
s.Eq s.Eq = <Prout s.Eq ' = ' s.Eq>;
s.L s.R = <Prout s.L ' # ' s.R>;
}
- Два указателя на одну функцию равны.
- Два указателя на разные функции не равны.
- Два замыкания, построенные из разных вложенных функций (одна в первой строчке, другая во второй), не равны.
- Два замыкания с разным контекстом не равны.
- Замыкания построены из одной и той же вложенной функции, захватывают равный контекст. Правила нет, не определено. В актуальной реализации при компиляции без оптимизаций будут не равны.
- Замыкания построены из одной и той же вложенной функции, захватывают равный (пустой) контекст. Правила нет, не определено. В актуальной реализации при компиляции без оптимизаций будут равны.
ВНИМАНИЕ!!! В актуальной реализации в древесных оптимизациях есть ошибка — нарушается инвариант равенства копий для замыканий. Про неё уже есть issue в багтрекере:
- Переопределений переменных в классическом Рефале-5 нет.
- В Рефале-5 имена переменных не могут начинаться на
_
, соответственно, безымянных переменных в нём тоже нет. - Вложенных функций и указателей на функции в Рефале-5 тоже нет.
Многие из конструкций Рефала-5λ являются синтаксическим сахаром — во время компиляции преобразуются в комбинации более примитивных конструкций.
Понимать, во что преобразуется программа, нужно по двум причинам:
- Во-первых, отладочный дамп показывает состояние преобразованной программы, а не исходной.
- Во-вторых, понимание преобразований помогает писать более эффективные программы.
Следующие конструкции языка являются синтаксическим сахаром и преобразуются в более примитивные:
- Переопределённые и безымянные переменные — они переименовываются.
- Присваивания — они преобразуются в блоки.
- Блоки, в свою очередь, преобразуются в вызовы вложенных функций.
- Вложенные функции — они преобразуются в глобальные функции и операции создания замыканий.
- Есть режим компиляции, когда даже условия преобразуются в вызовы
вспомогательных функций. При запуске через
rlc
иrlmake
он отключён, при «голом» запускеrlc-core
он включён.
В результате преобразований получается программа на Базисном Рефале,
расширенном условиями и операциями создания замыканий. Присваивания, блоки
и вложенные функции преобразуются в обычные именованные функции Рефала,
с именами вида 〈ИМЯ〉〈СУФФИКСЫ〉
, где 〈ИМЯ〉
— имя функции, в которой они
были определены, а 〉СУФФИКСЫ〉
— последовательности вида
〈ЗНАК〉〈ЧИСЛО〉〈ЗНАК〉〈ЧИСЛО〉…
, которые описывают «путь» к соответствующей
конструкции языка.
Можно выделить следующие проходы:
- Переименования переменных
- Рассахаривание присваиваний
- Рассахаривание блоков
- Рассахаривание вложенных функций
- (Рассахаривание условий)
Примечание. В файле Desugaring.ref перечислены другие проходы преобразований. В частности, там есть технические проходы, упрощающие дерево, например, удаление координат некоторых объектов. А проходы устранения присваиваний и блоков объединены в один.
Просмотреть результат рассахаривания можно в логе компиляции. Для этого нужно
запустить компилятор rlc
с опцией --log=имяфайла.log
(а rlmake
—
с пробросом опции компилятору: -X--log=имяфайла.log
). В логе будет выписан
результат преобразований в виде псевдокода на Рефале.
На этом проходе к каждой переменной приписывается уровень вложенности, на котором эта переменная была создана. Новый уровень вложенности формирует каждый следующий образец: условие, присваивание, блок, вложенная функция.
Благодаря этой нумерации переменная e.X
снаружи и переопределённая
переменная e.X^
где-то внутри просто получат разные номера.
Безымянные переменные просто получают уникальные имена, которых нет на текущем уровне вложенности.
Пример. Функция Eq
будет преобразована так:
Eq { → Eq {
(e.X) (e.Y) → (e.X#1) (e.Y#1)
= e.Y → = e.Y#1
: { → : {
e.X = True; → e.X#1 = True;
e.X^ = False; → e.X#2 = False;
} → }
} → }
Переопределённая переменная во втором предложении просто получила другое имя.
О переименовании знать полезно, поскольку при использовании отладочной опции
--markup-context
имена добавляются в дамп.
Присваивание преобразуется по довольно простому правилу. Было:
= …расш. результат… : …левая часть… = …остаток предложения…;
Становится:
= …расш. результат… : { …левая часть… = …остаток предложения… };
Нетрудно убедиться, что это преобразование сохраняет семантику.
Действительно, в исходном варианте, если сопоставление с левой частью успешно, то результат остатка предложения становится результатом всего предложения. Если сопоставление неуспешно, то программа аварийно завершается.
В новом варианте вызывается вложенная функция. Если сопоставление с левой частью единственного предложения успешно, то результат остатка предложения становится результатом вложенной функции, а значит, и преобразованного предложения. Если сопоставление неуспешно, то предложение последнее и функция падает аварийно.
Блок — это просто сокращённая запись для вызова функции по определению. Следующие две конструкции по определению эквивалентны:
…результат… : { …тело функции… }
<{ …тело функции… } …результат…>
Если блоков несколько, то они «наматываются» на результатное выражение последовательно:
…результат… : { …блок 1… } : { …блок 2… } : { …блок 3… }
<{ …блок 3… } <{ …блок 2… } <{ …блок 1… } …результат…>>>
Продолжая предыдущий пример, получим:
Eq { → Eq {
(e.X#1) (e.Y#1) → (e.X#1) (e.Y#1)
= e.Y#1 → = <
: { → {
e.X#1 = True; → e.X#1 = True;
e.X#2 = False; → e.X#2 = False;
} → }
} → e.Y#1
→ >
→ }
На этом этапе для каждой вложенной функции создаётся глобальная функция с почти тем же телом, что и исходная. А на место вложенной функции помещается конструкция построения замыкания.
Вложенные функции могут содержать переменные из окружающей области видимости. А значит, для вычисления функции эти переменные где-то должны храниться.
Хранятся они в объекте замыкания. Вообще, объект замыкания хранит две вещи: указатель на функцию, реализующую логику замыкания и связанные переменные контекста.
Рассахариватель просто берёт тело вложенной функции и в начало образца каждого предложения добавляет имена захваченных переменных, причём e-переменные заключаются в скобки. На место самой вложенной функции помещается конструктор замыкания, содержащий указатель на новую функцию и значения тех же переменных.
Пример:
F {
e.X = { e.Y = e.X '*' e.Y }
}
G {
e.X '+' e.Y
= {
X = e.X;
Y = e.Y;
}
}
↓ ↓ ↓ ↓ ↓ ↓
F {
e.X#1 = {{ &F\1 (e.X#1) }};
}
F\1 {
(e.X#1) e.Y#2 = e.X#1 '*' e.Y#2;
}
G {
e.X#1 '+' e.Y#1 = {{ &G\1 (e.X#1) (e.Y#1) }};
}
G\1 {
(e.X#1) (e.Y#1) X = e.X#1;
(e.X#1) (e.Y#1) Y = e.Y#1;
}
Пример с функцией F\1
демонстрирует, зачем нужно заключать в скобки
e-переменные, взятые из окружения.
Пример с Eq
будет выглядеть вот так:
Eq {
(e.X#1) (e.Y#1) = <{{ &Eq:1 (e.X#1) }} e.Y#1>;
}
Eq:1 {
(e.X#1) e.X#1 = True;
(e.X#1) e.X#2 = False;
}
Конструкторы замыканий мы здесь изобразили как двойные фигурные скобки, именно так они отображаются в отладочном дампе и в логе компиляции. Это обозначение условное, поскольку в исходном тексте написать этот конструктор нельзя — только вложенную функцию.
Семантику замыкания можно условно описать так:
<{{ &Func e.X }} e.Y> → < &Func e.X e.Y>
Т.е. если замыкание оказывается за левой скобкой вызова, на его место помещается содержимое объекта замыкания и повторяется попытка вызвать функцию. Поскольку справа от открывающей скобки вызова теперь уже находится указатель на фукнцию, функция вызывается.
Почему у функций F\1
и G\1
суффикс \1
, а у функции Eq:1
— :1
, мы
обсудим позже. Кратко: потому что в первом случае это просто вложенные функции
в результатном выражении, во-втором вложенная функция построена из блока.
Для упрощения отладки компилятор поддерживает режим разметки замыканий. Он
включается опцией --markup-context
. В этом случае захваченные переменные
в замыканиях предваряются идентификаторами с именами переменных. Пример
функции G
(см. выше) с такой разметкой:
G {
e.X#1 '+' e.Y#1 = {{ &G\1 "e.X#1:" (e.X#1) "e.Y#1:" (e.Y#1) }};
}
G\1 {
"e.X#1:" (e.X#1) "e.Y#1:" (e.Y#1) X = e.X#1;
"e.X#1:" (e.X#1) "e.Y#1:" (e.Y#1) Y = e.Y#1;
}
На быстродействии это почти не сказывается, но зато заметно упрощает отладку.
Дело в том, что условия в Рефале-5λ появились в два этапа. Сначала был написан преобразователь условий в базовое подмножество, поскольку ни кодогенератор, ни рантайм не умели с условиями работать. А потом была реализована полная поддержка условий и для сравнения производительности двух реализаций они были сделаны двумя независимыми режимами. Подробнее об этом можно прочитать по ссылкам
https://github.com/bmstu-iu9/refal-5-lambda/blob/master/doc/Презентация_Козлова_Условия_2018.pdf https://github.com/bmstu-iu9/refal-5-lambda/blob/master/doc/РПЗ_Козлова_Условия_2018.pdf
Полноценная реализация условий оформлена как режим оптимизации -OC
и эта
оптимизация по умолчанию включается в rlc
и rlmake
. Режим работы без
-OC
считается устаревшим и не рекомендуется к использованию, т.к. он более
медленный.
Однако, основной разработчик компилятора данный режим пока сохраняет, поскольку считает, что он ещё может пригодиться.
Включить режим рассахаривания условий можно ключом -OC-
для rlc
или
-X-OC-
для rlmake
. В этом случае будут создаваться вспомогательные
функции с суффиксами ?n?m
, где ?n
— порядковый номер условия в предложении
(см. следующий параграф), ?m
— номер вспомогательной функции для этого
условия. Вспомогательных функций как минимум два, для условий при образцах
с открытыми e-переменными их может быть больше.
Вернёмся к суффиксам. У функций F\1
и G\1
суффикс \1
, а у функции Eq:1
суффикс :1
. Почему и что они обозначают?
Доступны следующие суффиксы:
$n
— n-е предложение функции. Если в функции только одно предложение, то суффикс не добавляется.\n
— n-e замыкание в результатном выражении.=n
— n-e присваивание в выражении.?n
— n-е условие в выражении. Условия в дампе отображаются какУ присваиваний и условий сквозная нумерация.<Func …аргумент… <Func?n …аргумент условия…>>
:n
— n-й блок при расширенном результатном выражении.*n
— создаётся при оптимизации встраивания/прогонки.@n
— создаётся при оптимизации специализации.
Суффиксы *n
и @n
мы здесь не будем рассматривать. Заметим, лишь, что если
упала функция с суффиксом Func*n
или Func@0
(только ноль!), то на суффикс
можно не обращать внимания, а просто считать, что упала Func
. В случае
Func@n
, где n ≠ 0 всё сложнее — реальный аргумент функции будет существенно
перетасован. Нужно по логу компиляции разбираться во что компилятор переписал
исходную функцию Func
и её вызов. Но непосредственно на вызове Func@n
,
где n ≠ 0, программа падать не должна.
Пример. В функциях ниже в комментариях указаны имена неявных функций:
/* одно предложение */
F {
…pattern…
= …result… : …pattern… /* F=1 */
= { /* F=2\1 */
…pattern… = { /* F=2\1$1\1 */ }
…pattern… = { /* F=2\1$2\1 */ }
}
{ /* F=2\2 */
…pattern…
= …result… : …pattern… /* F=2\2=1 */
= …result…;
}
: …pattern… /* F=2 */
= { /* F\1 */ }
}
/* два предложения */
G {
…pattern…
, …result… : …pattern… /* G$1?1 */
= …result… : …pattern… /* G$1=2 */
, …result… : …pattern… /* G$1?3 */
, …result…
: {
/* G$1?4:1 */
}
: {
/* G$1?4:2 */
}
: …pattern… /* G$1?4 */
= …result…;
…pattern…
= …result…
: {
/* G$2:1 */
}
: {
/* G$2:2 */
};
…pattern…
, …result…
{
/* G$3:1 */
};
}
Таким образом, по суффиксу функции можно быстро локализовать соответствующую конструкцию языка.
Программ без ошибок не бывает, поэтому любая практически применимая реализация языка программирования должна предоставлять возможности для поиска и диагностики ошибок. В этом разделе мы рассмотрим основные средства и приёмы отладки программ на Рефале-5λ.
В Рефале-5λ имеются два основных диагностических средства: аварийный отладочный дамп и сборка с отладочным рантаймом. Кроме того, в этом разделе мы дадим советы по стилю программирования, минимизирующему количество ошибок.
Основным средством поиска и диагностики ошибок является аварийный дамп. Если
для некоторого вызова функции не удалось сопоставить аргумент ни с одним
из образцов, выполнение программы прерывается и на stderr
(по умолчанию)
выводится ошибочное первичное активное подвыражение, всё поле зрения, значения
статических ящиков и загруженные модули программы. Похожий дамп выводится,
если программе не хватило памяти (в этом случае дамп может быть очень длинным),
либо если произошла ошибка при вызове встроенной функции (например, деление
на ноль в Div
и Mod
или файл для открытия в Open
не существует).
При использовании отладочного рантайма можно указать имя файла для вывода дампа, также можно настроить параметры вывода, например, вывод маркеров области видимости. Подробнее мы рассмотрим всё это в следующем параграфе.
Обычно отладочного дампа бывает достаточно — повторно вызываем программу,
перенаправив stderr
в файл и пытаемся по дампу понять, что же тут не так. Либо
упавшей функции передаётся неправильный аргумент — в этом случае нужно искать
все вызовы этой функции, либо аргумент правильный, но ошибка уже в самой
функции.
Компиляция с отладочным рантаймом подключается при помощи опции --debug
,
передаваемой rlc
или rlmake
. При использовании префиксов (--rich
или
--slim
) к программе подключается префикс, уже собранный с отладочным
рантаймом. При сборке из исходников (--scratch
) подключаются файлы рантайма
с отладочным кодом.
Отладочный рантайм содержит несколько простых средств для вывода дополнительной диагностики и преждевременного аварийного прерывания программы. Все эти средства по умолчанию отключены, включаются установкой соответствующих опций в конфигурационном файле.
Программа, собранная с отладочным рантаймом, читает конфигурацию диагностики
по умолчанию из файлов @refal-5-lambda-diagnostics.ini
и [email protected]
, где progname
— имя исполнимого
файла программы. При запуске на Windows имя программы должно содержать
расширение .exe
: [email protected]
. Файлы должны
располагаться в текущем каталоге. Любой из них может отсутствовать. Если
присутствуют оба файла, то читаются оба, установки из файла с именем программы
имеют приоритет.
Если в командной строке программы имеются опции вида
++diagnostic+config=filename.ini
, то файлы *@refal-5-lambda-diagnostics.ini
читаться не будут — вместо них конфигурация диагностики будет загружаться
из ++diagnostic+config=…
. Если таких опций несколько, то параметры
из последующих имеют более высокий приоритет. Опции ++diagnostic+config=…
невидимы для выполняемой программы: среди аргументов, возвращаемых функцией
Arg
, они отсутствуют.
Формат конфигурационного файла диагностики следующий:
# Строки, которые начинаются на # — комментарии
# Ниже перечислены опции с их значениями по умолчанию
print-statistics = false
start-step-trace = 0
idents-limit = 0
memory-limit = 0
step-limit = 0
dump-free-list = false
show-cookies = true
show-hidden-steps = false
enable-debugger = false
enable-profiler = false
dump-file = ""
Регистр имён опций не имеет значения, знаки _
и -
в них взаимозаменяемы
(т.е. IDENTS_LIMIT
и Idents-Limit
означают одну и ту же опцию). Логические
значения могут записываться словами true
и false
(без учёта регистра) или
числами 1
или 0
. Строковые значения записываются в двойных кавычках,
однако их можно опускать.
Рассмотрим каждую из опций отдельно.
Если этот параметр установлен, программа после завершения своей работы выводит
на stderr
информацию о времени работы, количестве шагов и использованной
памяти. Например:
Total program time: 35.32800 seconds (100.0 %).
(Total refal time): 29.41900 seconds (83.3 %).
Linear pattern time: 12.81800 seconds (36.3 %).
Linear result time: 11.58300 seconds (32.8 %).
Native time: 3.69600 seconds (10.5 %).
t- and e-var copy time: 2.48500 seconds (7.0 %).
Open e-loop time (clear): 2.25100 seconds (6.4 %).
Runtime time overhead: 1.71500 seconds (4.9 %).
Context copy time: 0.49800 seconds (1.4 %).
Repeated e-var match time (inside e-loops): 0.25100 seconds (0.7 %).
Repeated t-var match time (inside e-loops): 0.03100 seconds (0.1 %).
Step count 26657583
Identifiers allocated: 1868
Memory used 549000 nodes, 549000 * 16 = 8784000 bytes
Для времени работы программы отдельно выписываются различные виды затрат, в скобках указывается процент от полного времени работы. Если затраты времени на некоторую компоненту оказались равны нулю (меньше погрешности измерения времени), соответствующая строчка не печатается.
Рассмотрим каждую из строчек подробнее:
Total program time
— общее время выполнения, от запуска до завершения.Native time
— время выполнения функций, написанных вручную на С++. Это прежде всего встроенные функции.(Total refal time)
— суммарное время выполнения функций, написанных на Рефале. В сумме сBuiltin time
должно давать 100 %.Linear pattern time
— «линейное» время сопоставления с образцом. Это то время, которое зависит только от вида образца и не зависит от аргумента. Из него исключается время, затраченное на сопоставление с повторными t- и e-переменными, а также время внутри циклов удлинения открытых e-переменных.Linear result time
— «линейное» время построения правой части. Тоже та его компонента, которая зависит только от вида образца и не зависит от значений переменных — исключается время копирования повторных переменных.Open e-loop time (clear)
— время, затраченное на циклы удлинения открытых переменных без учёта сопоставления повторных t- и e-переменных внутри цикла.Repeated ?-var match time (???? e-loops)
— время на сопоставление повторных t- и e-переменных. Вместо?
записывается литераt
илиe
, вместо????
— словоinside
илиoutside
. Соответственно, это время выполнения внутри и вне цикла удлинения открытых e-переменных. В примере выше сопоставления с повторными e-переменными внутри циклов удлинения потребовали 0,031 секунду, сопоставления с t-переменными и с e-переменными вне циклов потребовали времени меньше погрешности измерения (формально — ноль секунд), поэтому не напечатались.t- and e-var copy time
— время копирования переменных при построении правой части.Step count
— количество шагов рефал-машины.Identifiers allocated
— количество символов-слов, выделенных в программе. Включает в себя как символы-слова, непосредственно записанные в тексте программы, так и слова, созданные функциямиImplode
иImplode_Ext
.Memory used
— количество узлов, количество узлов × размер узла = объём затраченной памяти в байтах. Узел имеет размер 16 байт при компиляции в 32-разрядный код и 32 байта — в 64-разрядный.
«Линейное» время сопоставления с образцом и построения результата зависит только от вида левых и правых частей, остальные компоненты — циклы удлинения, сравнение на равенство повторных переменных, копирование переменных в правой части — зависят и от входных данных.
Число шагов, отображаемое в Step count
, отличается от значения, возвращаемого
функцией <Step>
в бо́льшую сторону. Функция Step
подсчитывает число шагов,
которые сделал классический Рефал-5 при выполнении данной программы. При этом
предполагается, что встроенные функции Рефала-5 выполняются за один шаг. Однако,
в Рефале-5λ некоторые «встроенные» функции Рефала-5 написаны на Рефале
и фактически выполняются за несколько шагов.
Параметры start-step-trace
и step-limit
используют реальное количество
шагов.
Иногда по состоянию поля зрения на момент ошибки трудно понять, откуда же эта
ошибка взялась, как сформировался некорректный аргумент для функции. Параметр
start-step-trace
позволяет увидеть временной контекст ошибки — некоторое
количество шагов, предшествующее падению. Если макрос установлен и равен
неотрицательному числу n, то начиная с n-го шага на stderr
(по умолчанию)
будет на каждом шаге выводиться дамп поля зрения (в том же формате, что и при
ошибке).
Поэтому, если непонятен контекст ошибки, установите параметр start-step-trace
равным числу, немного меньшему, чем номер шага, который привёл к ошибке.
На сколько меньшему — определяется опытным путём.
Также макрос может быть полезен при отладке зависаний. Если программа после
правки начала работать аномально долго — есть подозрение, что она вошла
в бесконечный цикл (бесконечную рекурсию), то можно установить достаточно
большой номер шага и, перенаправив stderr
в файл, прибить (Ctrl-C) процесс
спустя некоторое время. Скорее всего, в дампе будет виден искомый бесконечный
цикл.
Eсли эта опция установлена в ненулевое значение, то программа аварийно
остановится при создании указанного количества идентификаторов
(см. Identifiers allocated
выше).
Дело в том, что память для идентификаторов, созданных функциями Implode
и Implode_Ext
, никогда не освобождается. Поэтому, если, например, создавать
всё новые и новые идентификаторы в цикле, то память будет утекать. Опция
позволяет отлаживать такие утечки памяти.
Макрос должен принимать целочисленное значение. Если значение ненулевое, то при
превышении объёма распределённой памяти (в узлах, см. выше про Memory used
)
программа будет останавливатся с ошибкой недостатка памяти, даже если в системе
памяти достаточно.
Параметр предназначен для отладки программ, которые зависают с потреблением памяти в бесконечном цикле — остановка при достижении порога позволяет их прервать раньше.
Параметр позволяет прервать программу при достижении указанного количества шагов. Нулевое значение означает отсутствие ограничения.
Параметр помогает отлаживать зависающие программы — если программа выполняется
аномально долго, то прерывание по step-limit
, скорее всего, окажется внутри
бесконечного цикла.
Если этот параметр установлен, то в отладочном дампе выводится не только
первичное активное подвыражение и поле зрения, но и содержимое списка свободных
узлов — области памяти, где распределяется новое значение результатного
выражения (см. первую главу раздела). Параметр влияет на дамп, выводимый как
при ошибке, так и при установленном
макросе start-step-trace
.
Параметр предназначен для отладки рантайма и функций, написанных на C++, но может быть полезен для программ, которые почему-то требуют много памяти и вылетают от её недостатка.
Параметр включает отображение маркеров области видимости в отладочном дампе.
Маркеры записываются после имён функций как FuncName#MMM:NNN
, где MMM
и NNN
— десятичные числа. Каждая такая пара чисел уникальна для единицы
трансляции программы.
Список всех единиц трансляции, загруженных в программе, также выводится в отладочном дампе после дампа поля зрения.
Параметр полезен, если различные единицы трансляции имеют одноимённые локальные функции и нужно понять, функция из какого файла упала.
Параметр show-hidden-steps = BOOL
Если этот параметр установлен, то в дампе по start-step-trace
будут
отображаться шаги реализации встроенных функций, а также функций INIT
и FINAL
. Если не установлен — встроенные функции будут изображаться
как «одношаговые».
Если этот параметр установлен, при запуске программы будет включаться
консольный пошаговый отладчик (типа gdb
или reftr
). Отладчик на данный
момент работает нестабильно, поэтому мы не рекомендуем им пользоваться
и не будем описывать в этом справочнике.
Если это параметр установлен, то компилятор подсчитывает число запусков
и время выполнения каждой из функций. При завершении программы в текущей папке
создаются файлы _profile_time.txt
и _profile_count.txt
с суммарным
временем работы и суммарным количеством вызовов каждой из функций. Если
суммарное время работы функции равно нулю (меньше погрешности измерений),
то она в _profile_time.txt
не отображается.
Если этот параметр установлен, то отладочный дамп, выводимый при аварийном
остановке или при включённой опции start-step-trace
, будет выводиться
не на stderr
, а в указанный файл. Пустая строка означает, что дамп выводится
на stderr
.
Его бессмысленно использовать для отладки кода на Рефале. По следующим причинам:
- В отладчике будет виден не код на Рефале, а сгенерированный код на C++. Он написан в терминах элементарных операций сопоставления с образцом и построения результата, а потому читать и понимать его сложно. Тем более, что для этого нужно знать модель генерации кода.
- Поле зрения представляет собой двусвязный список, который смотреть в отладчике очень неудобно.
- Сгенерированные функции, как правило, очень длинные, их читать и понимать сложнее.
Однако, отладчик полезен при отладке функций, которые сами пишутся на C++, или рантайма. В частности, автор использовал GDB для просмотра точки падения и трассировки стека при ошибках доступа к памяти (SEGFAULT’ах).
Средств для отладки у Рефала-5λ немного, поэтому при поиске и устранении ошибок
в программах часто приходится прибегать к довольно примитивному инструменту —
отладочной печати. Заключается она, как не трудно догадаться, во вставке
в программу операций вывода — либо Prout
для печати на консоль, либо Putout
для печати в файл.
Поскольку открывать файлы в Рефале-5λ (как и в Рефале-5) необязательно, простота
вывода в файл сравнима с простотой вывода на консоль — вместо Prout
пишем
Putout
с неиспользуемым в программе номером. А дальше просто смотрим текстовый
файл REFAL*.DAT
.
В отладочном выводе можно просто фиксировать факт выполнения некоторого предложения функции либо можно выводить значения переменных (поскольку объектные выражения выводятся в читабельном виде). Но есть приём, который позволяет распечатывать каждый вызов функции. Рассмотрим его на примере.
Допустим, мы хотим распечатать каждый вызов функции DoFib
из листинга ниже:
Fibonacci {
1 = 1;
s.N = <DoFib 2 s.N 1 1>;
}
DoFib {
s.N s.N s.Prev s.Cur = s.Cur;
s.K s.N s.Prev s.Cur =
<DoFib <Add 1 s.K> s.N s.Cur <Add s.Prev s.Cur>>;
}
Функция имеет два предложения, поэтому в принципе можно добавить отладочную печать в каждое из них. Но в общем случае предложений может быть больше и добавлять печать в каждое будет утомительно. Поэтому поступим иначе — добавим вспомогательную функцию, которая послужит оболочкой к нашей исходной.
Для этого сначала переименуем DoFib
, например, в DoFib-DEBUG
.
DoFib-DEBUG {
s.N s.N s.Prev s.Cur = s.Cur;
s.K s.N s.Prev s.Cur =
<DoFib <Add 1 s.K> s.N s.Cur <Add s.Prev s.Cur>>;
}
Отлаживаемая функция имеет формат <DoFib s.K s.N s.Prev s.Cur>
. Добавим
новую функцию с именем DoFib
, левая часть которой совпадает с форматом
исходной. В правой части запишем вызов переименованной функции.
Важно. В общем случае, если отлаживаемая функция имела модификатор
$ENTRY
, функция-оболочка с отладочной печатью тоже должна быть entry.
Удобно это делать перед исследуемой функцией:
DoFib {
s.K s.N s.Prev s.Cur =
<DoFib-DEBUG s.K s.N s.Prev s.Cur>;
}
DoFib-DEBUG {
s.N s.N s.Prev s.Cur = s.Cur;
s.K s.N s.Prev s.Cur =
<DoFib <Add 1 s.K> s.N s.Cur <Add s.Prev s.Cur>>;
}
Теперь в функцию DoFib
можно добавить вывод любой отладочной печати,
например, всех переменных.
DoFib {
s.K s.N s.Prev s.Cur =
<Putout 13 '<DoFib>'>
<Putout 13 ' s.K = ' s.K>
<Putout 13 ' s.N = ' s.N>
<Putout 13 ' s.Prev = ' s.Prev>
<Putout 13 ' s.Cur = ' s.Cur>
<Putout 13>
<DoFib-DEBUG s.K s.N s.Prev s.Cur>;
}
DoFib-DEBUG {
s.N s.N s.Prev s.Cur = s.Cur;
s.K s.N s.Prev s.Cur =
<DoFib <Add 1 s.K> s.N s.Cur <Add s.Prev s.Cur>>;
}
В этом примере отладочная печать будет писаться в файл REFAL13.DAT
, который
будет открыт автоматически (при этом программист должен быть уверен, что файл
№ 13 не используется в данный момент).
Если отладочная функция располагалась перед исходной, то её довольно легко
удалить — нужно стереть строки от DoFib
(не включая) до DoFib-DEBUG
(включая), удаляемые строки помечены крестиком:
DoFib {
× s.K s.N s.Prev s.Cur =
× <Putout 13 '<DoFib>'>
× <Putout 13 ' s.K = ' s.K>
× <Putout 13 ' s.N = ' s.N>
× <Putout 13 ' s.Prev = ' s.Prev>
× <Putout 13 ' s.Cur = ' s.Cur>
× <Putout 13>
× <DoFib-DEBUG s.K s.N s.Prev s.Cur>;
× }
×
× DoFib-DEBUG {
s.N s.N s.Prev s.Cur = s.Cur;
s.K s.N s.Prev s.Cur =
<DoFib <Add 1 s.K> s.N s.Cur <Add s.Prev s.Cur>>;
}
Рефал — динамически типизируемый язык, только этим лучше не злоупотреблять. Любые злоупотребления вредны, в частности злоупотребления динамической типизацией затрудняют понимание программы, а значит, увеличивают вероятность появления ошибок.
Одной плохих практик при программировании на Рефале является смешивание
основной («интерфейсной») и вспомогательной функций в одном определении.
Например, функцию Fibonacci
из примера выше можно было бы написать так:
* ЭТО ПЛОХОЙ ПРИМЕР, НЕ ПИШИТЕ ТАК!!!
Fibonacci {
1 = 1;
s.N = <Fibonacci 2 s.N 1 1>;
s.N s.N s.Prev s.Cur = s.Cur;
s.K s.N s.Prev s.Cur =
<Fibonacci <Add 1 s.K> s.N s.Cur <Add s.Prev s.Cur>>;
}
Здесь мы объединили функции Fibonacci
и DoFib
, пользуясь тем, что они
имеют разные форматы.
Чем это плохо? Тем, что у функции теперь нет простого и прозрачного формата. Тем, что разные предложения используются с разными целями: первые два для вызова извне, вторые два — для рекурсивного вызова. Тем, что если кто-то по ошибке вызовет функцию не с одним числом, а четырьмя, программа не свалится с ошибкой, а втихаря вычислит неправильный результат.
Но здесь мы имеем случай с разными несовместимыми форматами. Перепутать одно число и четыре трудно. Но если формат различается на один терм и/или содержит e-переменную на верхнем уровне, допустить ошибку становится проще (а обнаружить наоборот сложнее).
Другая порочная практика — использовать e-переменную в последнем предложении. Иногда переменные в последнем предложении не используются или используются в правой части в том же порядке, что и в левой. В таких случаях соблазнительно их заменить одной e-переменной. Например, функцию
* Это хороший пример
$ENTRY refal05c_PrintNotFound {
(NotFound e.FileName) =
<Prout 'COMMAND LINE ERROR: file ' e.FileName ' not found'>;
(Output e.FileName) = ;
(Source (e.Source) e.Output) = ;
}
можно «сократить» так:
* А ЭТО ПЛОХОЙ ПРИМЕР, НЕ ПИШИТЕ ТАК!!!
$ENTRY refal05c_PrintNotFound {
(NotFound e.FileName) =
<Prout 'COMMAND LINE ERROR: file ' e.FileName ' not found'>;
e.Other = ;
}
Функцию
* Это хороший пример
DoParseBlock {
t.ErrorList (e.References) (e.Sentences) (TkCloseBlock t.SrcPos) e.Tail =
(Sentences e.Sentences) t.ErrorList (e.References) e.Tail;
t.ErrorList (e.References) (e.Sentences) (TkEOF t.SrcPos) e.Tail =
(Sentences e.Sentences)
<EL-AddErrorAt
t.ErrorList t.SrcPos 'Unexpected EOF, expected "}"'
>
(e.References)
(TkEOF t.SrcPos) e.Tail;
t.ErrorList (e.References) (e.Sentences) e.Tokens =
<ParseSentence t.ErrorList (e.References) (e.Sentences) e.Tokens>;
}
«упростить» так (см. последнее предложение):
* А ЭТО ПЛОХОЙ ПРИМЕР, НЕ ПИШИТЕ ТАК!!!
DoParseBlock {
t.ErrorList (e.References) (e.Sentences) (TkCloseBlock t.SrcPos) e.Tail =
(Sentences e.Sentences) t.ErrorList (e.References) e.Tail;
t.ErrorList (e.References) (e.Sentences) (TkEOF t.SrcPos) e.Tail =
(Sentences e.Sentences)
<EL-AddErrorAt
t.ErrorList t.SrcPos 'Unexpected EOF, expected "}"'
>
(e.References)
(TkEOF t.SrcPos) e.Tail;
e.Args = <ParseSentence e.Args>;
}
Обе функции (конечно, в хороших примерах) взяты из исходников компилятора Рефала-05.
Чем это плохо? Тем, что если функция будет вызвана с неправильным аргументом, ошибка либо не проявится (программа не вывалится с дампом поля зрения), либо проявится гораздо позже — отлаживать будет сложнее.
При корректном завершении (когда поле зрения становится пассивным — не содержит
вызовов функций) программа завершается с кодом возврата 0
.
Если программа завершилась вызовом функции Exit
, то её кодом возврата будет
указанное значение с точностью до особенностей платформы. Например, на Linux
будут учитываться только младшие 8 бит кода возврата.
В случае аварийного останова код возврата будет следующим:
- ошибка невозможности отождествления — код
101
, - ошибка нехватки памяти — код
102
, - остановка по
step-limit
— код103
, - остановка по
idents-limit
— код104
, - другие внутренние ошибки — коды
150
…159
.