Open Mazdaywik opened 6 years ago
Ниже приводится возможная реализация частично на Рефале, частично на псевдокоде. К ней следует относиться как к наброску. Для наглядности будут рассмотрены только Try
и Raise
, поскольку ReRaise
несколько усложнит картину.
Обработку исключений можно сделать почти полностью библиотечной — с минимальными правками рантайма.
Функцию Try
можно вообще описать на Рефале:
Try {
s.Callable Catch s.Catcher = <Try-Aux <s.Callable> Catch s.Catcher>;
(s.Callable e.Arg) Catch s.Catcher = <Try-Aux <s.Callable e.Arg> Catch s.Catcher>;
}
Try-Aux {
e.Result Catch s.Catcher = e.Result;
}
Функция Try-Aux
выполняет две роли. Во-первых, ограничивает в поле зрения контролируемые вычисления. Контролируемая функция выполняется между &Try-Aux
и Catch
. Во-вторых, когда контролируемые вычисления заканчиваются (аргумент становится пассивным), она сама себя удаляет из поля зрения.
Функцию Raise
нужно описывать на Си++. Функция Raise
имеет тот же формат, что и s.Catcher
, поэтому в коде ниже будет опущена проверка аргумента — аргумент Raise
напрямую передаётся в s.Catcher
. Поле зрения на момент вызова Raise
имеет вид:
… <Try-Aux … <Raise t.FuncCall e.Message> … > …
Вот её код на Си++ с элементами псевдокода:
$ENTRY Raise {
%%
Iter pRaise = arg_begin->next;
Iter try_aux_begin, try_aux_end, try_aux_func;
do {
try_aux_begin = refalrts::pop_stack(vm);
if (try_aux_begin != 0) {
try_aux_end = refalrts::pop_stack(vm);
try_aux_func = try_aux_begin->next;
} else {
try_aux_func = 0;
}
while (try_aux_func != 0 && try_aux_func->function_info == &Try-Aux);
if (try_aux_func == 0) {
Распечатать при помощи Prout’а e.Message
Заменить вызов <Raise (s.Func e.Arg) e.Message> на <s.Func e.Arg>
return refalrts::cUnhandledException;
}
Iter sCatcher = try_aux_end->prev;
Iter pCatch = sCatcher->prev;
// поле зрения
// … <Try-Aux … <Raise arg> … Catch s.Catcher> …
refalrts::splice_stvar(pRaise, sCatcher);
// … <Try-Aux … <s.Catcher Raise arg> … Catch> …
refalrts::splice_to_freelist(pRaise, pRaise);
// … <Try-Aux … <s.Catcher arg> … Catch> …
refalrts::splice_to_freelist(try_aux_func, arg_begin);
// … <s.Catcher arg> … Catch> …
refalrts::splice_to_freelist(arg_end, pCatch);
// … <s.Catcher arg> …
refalrts::push_stack(vm, try_aux_end);
refalrts::push_stack(vm, try_aux_begin);
return refalrts::cSuccess;
%%
}
В рантайм потребуется добавить константу cUnhandledException
и функцию pop_stack()
. По смыслу cUnhandledException
ничем не отличается от cRecognitionImpossible
за исключением другого сообщения об ошибке. Функция pop_stack()
есть в реализации виртуальной машины, её нужно только протянуть в интерфейс.
Преимущество предложенной реализации в её простоте и эффективности. Простота в том, что она почти полностью библиотечная, а функция Try
пишется на Рефале. Эффективность — минимальные накладные расходы на случай без исключений, два лёгких шага рефал-машины.
Функция ReRaise
заметно усложняет реализацию. В предложенной реализации функция Raise
заменяет вызов Try-Aux
на вызов перехватчика, стирая всё поле зрения вокруг аргумента Raise
и внутри Try-Aux
, после чего вызывает перехватчик. Для корректной работы ReRaise
сначала должен быть запущен перехватчик (который может вызвать ReRaise
), а после работы перехватчика вызов Try-Aux
должен замениться на результат работы перехватчика.
Но это всё технические сложности, а не принципиальные. Даже с учётом ReRaise
правки рантайма окажутся минимальными и код без исключений выполнится эффективно. Реализация функции Try
, скорее всего, окажется такой же, но вот Raise
и ReRaise
будут страшнее.
Функция ReRaise
не особо нужна, по крайней мере на первом этапе.
Задача вполне приемлема для летней практики.
Если с ReRaise
, то задача достаточно сложная для курсовой. Если нет — маловато (но тоже можно).
А, вообще-то, с ReRaise
вполне приемлемая для курсовой. Без неё тоже приемлемая, но почти халявная, ведь решение подробно расписано.
Плохая задача для курсовой, даже с ReRaise
. Кода всего на 100 строк. В записке нечего писать будет, а надо 20 страниц. Хорошо, что никто не выбрал.
Есть один нюанс в предложенной выше реализации. Она не будет работать при использовании прямой кодогенерации, если исключение произошло в условии, а обработчик (Try-Aux
) снаружи условия.
Для выполнения условия интерпретатор в режиме прямой кодогенерации вызывается рекурсивно. Вложенный вызов завершается, когда выполняется функция-условие (FuncName?n
). Обработка исключения просто напросто перескакивает через этот вложенный вызов, а значит системный стек и стек Рефала рассогласуются. Если обработчик исключения сам находился в условии, то завершение этого условия приведёт к возврату в условие перепрыгнутой функции и возобновится не та функция, которая должна быть. Указатели на поле зрения, которые хранились в прерванной функции, будут уже указывать совсем не туда.
Решить эту проблему невозможно без переписывания поддержки условий или вообще генерации кода C++, причём не очевидно как. И такой задачи масштабной переработки сейчас не стоит.
Поэтому эта задача вообще замораживается на долгий срок. Если я придумаю, как совместить режим прямой кодогенерации с поддержкой исключений, то разморожу.
Кстати и в режиме интерпретации тоже не всё так просто. Если между Raise
и Try-Aux
находится одно или несколько условий, то нужно снять со стека соответствующее число запомненных состояний. Это крайне не элегантно, а вернее сказать, костыльно.
Поэтому разобранное выше в комментариях решение не подходит. Вернее, подходило бы только для базисного Рефала.
Поэтому другое решение. Если ранее Raise
несла всю логику, а Try
+ Try-Aux
были почти пустышкой, то теперь наоборот.
Функция Raise
ничего не делает, кроме как возвращает на стек скобок свои скобки и возвращает refalrts::cException
. Когда код прямой кодогенерации рекурсивно вызывает главный цикл, он проверяет код возврата рефал-машины. Если он не cSuccess
, значение возвращается как есть.
Функция Try
реализуется на уровне рантайма. Она рекурсивно вызывает Рефал-машину (для контролируемой функции), запоминая верхушку стека состояний и стека скобок активации. Если рекурсивный вызов завершился успешно, функция Try
просто оставляет в поле зрения результат контролируемой функции. Если не cException
, то завершает главный цикл с этим кодом возврата.
Если рекурсивный вызов вернул cException
, то на вершине стека скобок активации находится вызов <Raise …>
, на вершине стека состояний — лишние состояния для пропущенных условий. Функция Try
снимает со стека состояний лишние узлы (ведь она до этого сохранила предыдущую вершину), верхушку стека активаций она просто присваивает, из вызова Raise
берёт аргумент для обработчика. Затем удаляет лишний мусор (поле зрения вокруг бывшего <Raise …>
, но внутри <Try …>
) и вызывает обработчик с аргументом <Raise …>
.
Как реализовать ReRaise
? Мусор не удалять, подменять вызов <Raise …>
вызовом обработчика. А ReRaise
сделать синонимом Raise
. В этом случае текст в поле зрения сохранится, а значит, сохранится и контекст исключения. Только вопрос — а зачем это нужно? Можно придумать и более хитрую реализацию, с рекурсивным вызовом обработчика. И вместо cException
возвращать cRaise
и cReRaise
, чтобы различать эти случаи. В случае Raise
мусор удалять, в случае ReRaise
, удалять скобки активации <Try …>
, т.к. семантика такова, что исключение передаётся следующему обработчику, как если бы предыдущего не было. Но, ещё раз, зачем нужен этот кривой ReRaise
, ведь обработчик может подменить сообщение об исключении? Сценарий, когда полезен ReRaise
, мне не очевиден.
Но и с этим вариантом решения задача слишком маленькая для курсовой. И даже в этом варианте решения потребуются ≈100 строк кода. Да и исключения пока не особо нужны.
Мотивация
Встроенные функции ввода-вывода Рефала-5 не могут сообщить об ошибке, собственно, ввода-вывода. Если файла не существует, то
Open
аварийно остановит программу. Та же проблема будет при попытке писать файл, если на диске закончилось место. И т.д.Рефал-5 — экспериментальный язык, язык для исследований в сфере преобразования программ на Рефале (и как входной язык для таких преобразователей, и как язык реализации). Поэтому и библиотека встроенных функций ограничена некоторым достаточным минимумом, и сами функции имеют ограниченную семантику.
Рефал-5λ позиционируется как промышленный язык. А в промышленных языках ошибочные ситуации в операциях ввода-вывода нужно перехватывать и корректно обрабатывать. Аварийные падения допустимы для внутренних ошибок (вроде SEGFAULT’ов или деления на нуль), но не для нештатных ситуаций взаимодействия со внешней средой.
Хорошо известны два способа обработки подобных внештатных ситуаций — возврат кода ошибки и выбрасывание исключения (которое можно затем перехватить и обработать).
Возврат кода ошибки довольно просто и естественно реализуется в Рефале. Например, та же функция открытия файла (назовём её
SafeOpen
) могла бы иметь следующий формат:Но у этого подхода есть два недостатка:
Поэтому предлагается реализовать исключения.
Библиотечные средства для работы с исключениями
Никакой новый синтаксис не вводится, поскольку для работы с исключениями в минимальном варианте достаточно ввести три новых функции (разумеется, они потребуют правок рантайма).
Первая функция — это генерация исключения
Семантика. Если исключение не перехвачено (см. далее), то функция выводит в файл дампа
e.ErrorMessage
(в том же виде, что иProut
), заменяет свой вызов наt.FuncCall
(но с угловыми скобками) и возвращаетrefalrts::cRecognitionImpossible
. Таким образом, останов будет выглядеть, как будто упал вызовt.FuncCall
, но с дополнительным текстом ошибки. Пример.Напечатается
FileNotFound
, вызов<Raise …>
заменится на<Open 'r' 10 'file.txt'>
и программа остановится с выдачей аварийного дампа.Вторая функция — это конструкция перехвата исключения:
Семантика. Первый аргумент функции
t.Callable
— вызываемая функция, которая может упасть. Второй аргумент — обработчик исключения. Если функция, заданная вt.Callable
, завершилась без исключений, то её возвращаемое значение совпадает с возвращаемым значением функцииTry
. Если исключение произошло (была где-то вызвана функцияRaise
), то вызываетсяs.CatchFunction
с аргументом вызоваRaise
. Возвращаемое значение обработчика становится возвращаемым значением функцииTry
. Пример. Немножко надуманный, но иллюстрирует.В примере встретилась третья функция —
ReRaise
, которая служит для продолжения обработки исключения внешним перехватчиком.Семантика. Может быть вызвана только из
s.CatchFunction
. Передаёт управление наружному обработчику исключения, как если бы предыдущего обработчикаTry
не было. Если больше нет обработчиков, программа выводит аварийный дамп, если есть — исключение перехватывается в следующем из них.Преимущества выбранного подхода:
Putout
, то в дампе будет виден ошибочный вызовPutout
, а неAutoopen
илиPutout-Aux
.Какие ошибки перехватывать?
Очевидно, нужно описывать исключениями ошибки функций ввода-вывода (см. мотивацию).
Технически можно генерировать исключение даже для ошибок невозможности отождествления или недостатка памяти (хотя здесь засада в том, что как программа будет продолжать работу, если память закончилась), но надо ли? На мой взгляд — скорее, не надо. Внутренние ошибки должны приводить к падениям, скрывать их нельзя. Но, с другой стороны, операционные системы и языки программирования позволяют перехватывать и ошибки доступа к памяти, и ошибки деления на нуль, значит, это зачем-то может быть нужно.
С делением на нуль тоже не очевидно. Функции длинной арифметики написаны на Рефале, поэтому сейчас сообщение об ошибке деления на нуль в дампе не очевидно. В дампе будет вызов не
<Div … 0>
, а одной из вспомогательных функций, затрудняюсь сказать какой (запускать лень). ФункцияRaise
могла бы прояснить сообщение об ошибке.Возможно, для ошибок в вызове встроенных функций (не только деление на нуль, но и любые другие нарушения формата) следует добавить неперехватываемый аналог
Raise
.Всё это надо обдумать.