
Иногда в голову приходят идеи, которые звучат ужасно, но требуют воплощения в жизнь. Несколько месяцев назад автор этой заметки, разработчик под ником ntietz*, обсуждала с другом идею языка, в котором единственным потоком команд является обработка ошибок. Эта мысль укоренилась в сознании автора и не давала ей покоя. Она продолжала приставать ко всем с этой идеей, пока в течение одной недели пара человек случайно не подтолкнули её реализации задумки.
К сожалению, ntietz решила воплотить этот язык в жизнь — и теперь заранее просит у читателей прощения. Если вы решите перейти под кат, знайте, что делаете это на свой страх и риск.
*Обращаем ваше внимание, что позиция автора может не всегда совпадать с мнением МойОфис.
Предпосылки создания Hurl
Вот основная идея этого языка.
Знаете ли вы, что в Python иногда используют исключения для контроля потока управления? Да, да, я в курсе, что это не основное их предназначение, но фактически так и происходит. Исключения имеют много общего с операторами goto, с помощью которых можно переходить в другое место программы. Но они менее гибкие, так как позволяют двигаться только
Отсюда следует наше первое интересное решение: инструкции заканчиваются точкой с запятой. Я предпочитаю точки с запятой, поскольку лично для меня они упрощают восприятие грамматики. В остальном это очень напоминает синтаксис JavaScript или Rust. По сути, я взяла готовое решение. Язык имеет динамическую типизацию, поэтому не требуется указывать тип каждого элемента. Это помогает уменьшить объём грамматики. Посмотрим, как это скажется на реализации интерпретатора! Следующий шаг — определение анонимных функций. Это делается с помощью ключевого слова Кстати, функция не может быть рекурсивной (вызывать саму себя), поскольку при её определении мы не будем привязывать её к имени в локальном контексте. Забавно, правда? Прекрасно. У нас есть функции. Теперь нам нужна вишенка на торте. Во-первых, мне очень жаль. Я не должна была этого делать, но сделала, и вот мы здесь. Обработка исключений состоит из двух этапов: генерация исключения и его перехват. Есть два способа сгенерировать исключение: Использовать Использовать Я знаю, как сложно использовать Рассмотрим пару примеров с анализом состояния стека для каждого случая. В первом примере мы создадим фиктивную функцию, которая вбрасывает ( Эта программа определит несколько функций, а затем выполнит первую ( Трассировка сложновата для восприятия (если вы знаете, как описать её лучше, поделитесь, чтобы я могла обновить публикацию и будущую документацию). Однако достаточно понимать её как «стандартную обработку исключений, плюс возможность вбросить всё что угодно». Также появилась ещё одна конструкция, А второй вариант довольно забавный. Это Вот укороченная трассировка, начинающаяся прямо с инструкции Вот и всё! Вот что нам нужно, чтобы создать полезный язык, способный выполнять все стандартные функции языков. У нас нет чёткого метода обработки ошибок, поскольку обработка исключений используется для реального потока управления. Так что будем аккуратными, чтобы не плодить ошибки и баги. Пришло время собрать всё воедино и сделать что-то «полезное». Условные операторы и циклы — это основы программирования. Как их выразить в моей парадигме? Условные операторы довольно просты, начнём с них. Мы можем просто вбросить ( Давайте, к примеру, определим значения, которые больше нуля. В результате будет выведено «over 0». Здесь оценивается условие, вбрасывается результат С циклами все немного сложнее. Рекурсия нам недоступна, поэтому приходится проявлять смекалку. Начнем с определения функции цикла. Эта функция должна сама принимать в себя функцию цикла в качестве аргумента. Она также должна принимать тело и локальные переменные цикла. Тело цикла должно удовлетворять условию: Оно должно отбросить ( Через некоторое время после этого оно должно Выглядит примерно так: А затем для его использования нужно определить тело. И если мы это вызовем, то сможем увидеть, что происходит! В результате должно получиться следующее: И это, в общем-то, всё, что нам нужно. Ещё один интересный пример: программа fizzbuzz. Если на языке программирования нельзя реализовать задачу fizzbuzz, то он бесполезен для тестирования. Поэтому мы должны удостовериться, что на нём можно написать решение этой задачи. Вот реализация с использованием ранее определённой функции По-моему, Crafting Interpreters («Создание интерпретаторов»). И это мой первый опыт разработки языка! Риски минимальны, поэтому я могу спокойно двигаться дальше, не привязываясь к чему-то конкретному. План заключается в итеративной работе над интерпретатором. Мои дальнейшие действия: Определить грамматику Разработать лексический анализатор Разработать парсер (демонстрация: проверка парсинга программ) Разработать форматтер (демо: переформатирование программ) Разработать интерпретатор Написать несколько программ для души (Advent of Code 2022?) и создать стандартную библиотеку В первую очередь я планирую разработать форматтер, поскольку все современные языки его используют. Его создание будет значительно проще, чем разработка интерпретатора, и я смогу быстрее приступить к работе. Разработка самого интерпретатора займет немало времени и будет состоять из нескольких итераций.Определение анонимных функций
func, как в Go или
Обработка исключений
hurl, который работает так, как и ожидалось: раскручивает стек по мере продвижения, пока не достигнет блока catch, соответствующего значению, или пока не исчерпает стек.toss, который работает немного иначе: обходит стек, пока не найдет соответствующий блок catch, но затем с помощью ключевого слова return можно вернуться (go back) к месту, где было сгенерировано исключение.return таким нестандартным способом. Еще раз прошу прощения, я не просила вас читать дальше. Но наградой для вас будет возможность увидеть, как это можно использовать для создания потока команд.hurls) значение, и перехватим его в вызывающей родительской функции. Я добавила номера строк для удобства отображения трассировки далее. 1 | let thrower = func(val) { 2 | hurl val + 1; 3 | }; 4 | 5 | let middle = func(val) { 6 | print("middle before thrower"); 7 | thrower(val); 8 | print("middle after thrower"); 9 | }; 10 | 11 | let first = func(val) { 12 | try { 13 | middle(val); 14 | } catch as new_val { 15 | print("caught: " + new_val); 16 | }; 17 | }; 18 | 19 | first(2);first). Ниже приведена примерная трассировка выполнения программы при вызове first(2):(file):19: stack: (empty) calls first first:12: stack: [ (first, 2) ] enters try block first:13: stack: [ (first, 2), (<try>) ] calls middle middle:6: stack: [ (first, 2), (<try>), (middle, 2) ] prints "middle before thrower" middle:7: stack: [ (first, 2), (<try>), (middle, 2) ] calls thrower thrower:2: stack: [ (first, 2), (<try>), (middle, 2), (thrower, 2) ] resolves val as 2, adds 1, and stores this (3) as a temp thrower:2: stack: [ (first, 2), (<try>), (middle, 2), (thrower, 2) ] hurls 3, pops current stack frame middle:7: stack: [ (first, 2), (<try>), (middle, 2) ] status: hurling 3 not in a try block, pops stack frame first:13: stack: [ (first, 2), (<try>) ] status: hurling 3 in a try block, try block matches, jump into matching block first:15: stack: [ (first, 2), (<try>), (<catch>, 3) ] print "caught: 3" pop catch and try stack frames pop first stack frame file:19: stack: [] execution completecatch as, которая позволяет перехватывать все значения и сохранять их в новой локальной переменной. Можно также использовать что-то вроде catch (true) или catch ("hello"), чтобы перехватывать только определённые значения.toss. Мы можем изменить приведённый выше пример, чтобы использовать toss и return. На этот раз я просто покажу стек с момента, когда мы доходим до toss; до этого программа выполняется аналогичным образом (с незначительными изменениями номеров строк). 1 | let thrower = func(val) { 2 | toss val + 1; 3 | }; 4 | 5 | let middle = func(val) { 6 | print("middle before thrower"); 7 | thrower(val); 8 | print("middle after thrower"); 9 | }; 10 | 11 | let first = func(val) { 12 | try { 13 | middle(val); 14 | } catch as new_val { 15 | print("caught: " + new_val) 16 | return; 17 | }; 18 | }; 19 | 20 | first(2);toss. Заметьте, теперь у нас есть индекс, который указывает текущую позицию в стеке. Он начинается с нуля, что соответствует языку, на котором я напишу интерпретатор.thrower:2: stack: [ (first, 2), (<try>), (middle, 2), (thrower, 2) ] stack index: 3 tosses 3 from stack index 3, decrements stack index middle:7: stack: [ (first, 2), (<try>), (middle, 2), (thrower, 2) ] stack index: 2 status: tossing 3 from stack index 3 not in a try block, decrements stack index first:13: stack: [ (first, 2), (<try>), (middle, 2), (thrower, 2) ] stack index: 1 status: tossing 3 from stack index 3 in a try block, try block matches, jump into matching block creating a substack first:15: stack: [ (first, 2), (<try>), (middle, 2), (thrower, 2) ] stack index: 1 status: tossing 3 from stack index 3 substack: [ (<catch>, 3) ] print "caught: 3" first:16: stack: [ (first, 2), (<try>), (middle, 2), (thrower, 2) ] stack index: 1 status: tossing 3 from stack index 3 substack: [ (<catch>, 3) ] returning, pop the substack, set stack index to 3 thrower:2: stack: [ (first, 2), (<try>), (middle, 2), (thrower, 2) ] stack index: 3 finish this function, pops current stack frame middle:8: stack: [ (first, 2), (<try>), (middle, 2) ] stack index: 2 prints "middle after thrower" finish this function, pops current stack frame first:13: stack: [ (first, 2), (<try>) ] stack index: 1 finishes the try block, pops current stack frame finish this function, pops current stack frame file:20: stack: [] stack index: 0 execution completeРеализация потока управления посредством обработки исключений
hurl) значение в блок try и использовать блоки catch для сравнения значений.let val = 10; try { hurl val > 0; } catch (true) { print("over 0"); } catch (false) { print("not over 0"); };true, и значение сразу же перехватывается. Если вдруг будет вброшено что-то отличное от true или false, продолжится дальнейшее раскручивание стека, поэтому будьте внимательны. Подумайте о возможности добавления catch как универсального перехватчика ошибок.
toss) локальные переменные следующей итерации перед окончанием тела цикла.hurl либо true (для запуска очередной итерации), либо false (для завершения итерации).let loop = func(loop_, body, locals) { try { body(locals); } catch as new_locals { try { // `return` goes back to where the locals were tossed from. // This has to be inside a new `try` block since the next things // the body function does is hurl true or false. return; } catch (true) { loop_(loop_, body, new_locals); } catch (false) { hurl new_locals; } }; };let count = func(args) { let iter = args[1]; let limit = args[2]; print("round " + iter); toss [iter + 1, limit]; hurl iter < limit; }loop(loop, count, [1, 3]);round 1 round 2 round 3Пример программы
loop.let fizzbuzz = func(locals) { let x = locals[1]; let max = locals[2]; try { hurl x == max; } catch (true) { toss locals; hurl false; } catch (false) {}; let printed = false; try { hurl ((x % 3) == 0); } catch (true) { print("fizz"); printed = true; } catch (false) {}; try { hurl ((x % 5) == 0); } catch (true) { print("buzz"); printed = true; } catch (false) {}; try { hurl printed; } catch (false) { print(x); } catch (true) {}; toss [x+1, max]; hurl true; }; loop(loop, fizzbuzz, [0, 100]);
ссылка на оригинал статьи https://habr.com/ru/articles/766724/
Добавить комментарий